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

bunload

v0.1.1

Published

A collection-based CMS and application backend for Bun and SQLite

Readme

bunload

Reusable Bunload runtime for Bun workspaces.

import { bunload } from "bunload";
import { defineConfig } from "bunload/schema";
import { createBunloadApi } from "bunload/server";
import { generateTypes, runMigrations } from "bunload/tooling";

The root bunload export is app-agnostic. After running bun run bunload generate:types, application code should import bunload from its generated file for collection autocomplete and automatic result/input inference:

import { bunload } from "./generated/bunload-types";

const users = await bunload.find({ collection: "users" });

Collections with auth: true receive managed email and password inputs plus typed session and password-lifecycle client methods. Passwords are hashed with Argon2id, sessions are stored server-side, and login uses an HttpOnly cookie. Optional recovery uses auth.resetPassword.sendResetToken so the application can deliver one-use tokens through its own email or messaging provider. Authentication does not itself restrict collection CRUD; access control is a separate concern.

Auth collections can enforce password composition and active-session limits, and can deliver redacted audit events to the application:

{
	slug: "accounts",
	auth: {
		maxSessions: 5,
		passwordPolicy: {
			minLength: 12,
			maxLength: 128,
			requireLowercase: true,
			requireUppercase: true,
			requireNumber: true,
			requireSymbol: true,
		},
		onAudit(event) {
			return auditWriter.write(event);
		},
	},
	fields: [],
}

Use bunload.sessions({ collection: "accounts" }) to list the current user's active sessions and bunload.revokeSession({ collection: "accounts", sessionId }) to revoke one. Audit events contain request metadata but never passwords, cookies, or bearer tokens.

HTTP security

Bunload enables security headers, CSRF origin checks for cookie-authenticated mutations, and an isolated in-memory rate limiter by default. Configure trusted browser origins explicitly when the API and UI use different origins:

export default defineConfig({
	security: {
		cors: {
			origins: ["https://app.example.com"],
			credentials: true,
		},
		csrf: {
			trustedOrigins: ["https://app.example.com"],
		},
		rateLimit: {
			windowMs: 60_000,
			max: 100,
			loginMax: 10,
			passwordResetMax: 5,
			getClientIp: (request) => request.headers.get("x-real-ip") ?? undefined,
			store: distributedRateLimitStore,
		},
		headers: {
			contentSecurityPolicy: "default-src 'none'; frame-ancestors 'none'",
			frameOptions: "DENY",
			referrerPolicy: "strict-origin-when-cross-origin",
			hstsMaxAge: 31_536_000,
		},
	},
	db: { filename: "db.sqlite3" },
	collections: [],
});

The configured rate-limit store receives consume(key, limit, windowMs) and must return { allowed, limit, remaining, resetAt }, which allows Redis or another shared store in multi-process deployments. IP and authenticated-identity limits are both applied. Only trust forwarded IP headers from a controlled reverse proxy. Credentialed cross-origin clients must be present in both cors.origins and csrf.trustedOrigins. HSTS is emitted only for HTTPS requests; terminate TLS before Bunload and preserve the external scheme when using a reverse proxy.

Use collection access rules for operation or row-level authorization and field access rules for read/write masking. Rules receive the authenticated user; row filters are always combined with client filters using AND. Collections without access rules remain public for backward compatibility.

Applications expose the package CLI through one script, matching Payload's project-local CLI pattern:

{
	"scripts": {
		"bunload": "bun x bunload"
	}
}
bun run bunload generate:types
bun run bunload migrate:create add-post-status
bun run bunload migrate:create rename-user --manual
bun run bunload migrate
bun run bunload migrate:status
bun run bunload migrate:status --json
bun run bunload backup backups/pre-deploy.sqlite3
bun run bunload restore backups/pre-deploy.sqlite3

The CLI discovers src/bunload.config.ts by default. Set BUNLOAD_CONFIG_PATH or pass --config for another location. The lower-level tooling entry point remains available for programmatic use.

Artifact paths can be configured alongside the collections:

export default defineConfig({
	db: {
		filename: "db.sqlite3",
		migrationDir: "migrations",
	},
	typescript: {
		outputFile: "src/generated/bunload-types.ts",
	},
	collections: [],
});

An application owns its config, migrations, generated types, and database:

import { serve } from "bun";
import { createBunloadApi } from "bunload/server";
import config from "./bunload.config";

const api = createBunloadApi(config);
serve({ routes: { "/api/:slug": api, "/api/:slug/:action": api } });

Each API owns its configuration and SQLite connection, so multiple instances can run in one process. Startup fails when migration files and the bunload_migrations ledger differ; apply pending migrations with bun run bunload migrate. The returned handler also exposes api.close() for tests and embedded runtimes.

Operations and observability

Configure a structured logger with debug, info, warn, and error methods. Bunload emits request, migration-check, authentication, and hook events. Entries use a fixed metadata shape and never include request bodies, headers, cookies, passwords, session tokens, or reset tokens. Logger failures do not affect requests.

export default defineConfig({
	logger: {
		debug: (entry) => logger.debug(entry),
		info: (entry) => logger.info(entry),
		warn: (entry) => logger.warn(entry),
		error: (entry) => logger.error(entry),
	},
	db: { filename: "db.sqlite3" },
	collections: [],
});

Every response includes X-Request-ID. A valid incoming X-Request-ID is preserved; otherwise Bunload generates a UUID. JSON error envelopes also include requestId.

Health checks are available through the normal API handler:

  • GET /api/_health/live checks the process without writing to SQLite.
  • GET /api/_health/ready checks SQLite and migration drift, returning 503 when the instance is not ready.

Use api.shutdown() to reject new API requests, wait for active requests and transactions, and close SQLite. shutdownBunloadApis(apis) drains multiple instances. Stop the Bun listener before draining:

import { createBunloadApi, shutdownBunloadApis } from "bunload/server";

const api = createBunloadApi(config);
const server = Bun.serve({
	routes: {
		"/api/:slug": api,
		"/api/:slug/:action": api,
	},
});

async function shutdown() {
	await server.stop(false);
	await shutdownBunloadApis([api]);
}

process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);

migrate:status --json prints { applied, pending, missing } for deployment gates. Run migrations before starting application processes.

backup [output] serializes a consistent SQLite snapshot and runs PRAGMA integrity_check. restore <backup> validates the source, replaces the destination through a temporary file, and verifies the result. Restore is offline: stop every process that can access the database first. Take and retain a verified pre-migration backup; SQL migrations are forward-only by default, and manual down functions are documentation/escape hatches rather than an automatic rollback mechanism. Restore the pre-migration backup when a deployed migration must be rolled back, then deploy code matching that schema.

CMS capabilities

Collections can opt into drafts, bounded version history, soft delete, custom endpoints, and uploads. These features reuse collection access rules, field access rules, hooks, and transactions:

import { defineConfig } from "bunload/schema";
import { LocalStorageAdapter } from "bunload/server";

export default defineConfig({
	localization: {
		locales: ["en", "pt"],
		defaultLocale: "en",
		fallbackLocale: "en",
	},
	db: { filename: "db.sqlite3" },
	collections: [
		{
			slug: "articles",
			drafts: { autosave: true, read: ({ user }) => Boolean(user) },
			versions: { maxPerDocument: 20 },
			softDelete: true,
			fields: [
				{ name: "title", type: "text", required: true, localized: true },
				{ name: "content", type: "richText" },
				{ name: "location", type: "point" },
				{
					name: "layout",
					type: "blocks",
					blocks: [{ slug: "hero", fields: [{ name: "heading", type: "text", required: true }] }],
				},
				{ name: "summary", type: "virtual", compute: ({ doc }) => String(doc.title ?? "") },
				{ name: "comments", type: "join", relationTo: "comments", on: "article" },
			],
		},
		{
			slug: "media",
			upload: {
				storage: new LocalStorageAdapter("uploads"),
				mimeTypes: ["image/*"],
				maxFileSize: 10 * 1024 * 1024,
			},
			fields: [{ name: "alt", type: "text", required: true }],
		},
	],
});

Localized inputs are objects keyed by locale. Pass locale to reads; locale: "all" returns the complete locale map. Fields support sync or async validate callbacks, including recursive paths inside groups, arrays, and blocks.

Published and non-deleted documents are returned by default. Use the generated client for editorial workflows:

const draft = await bunload.autosave({ collection: "articles", data: { title: { en: "Draft" } } });
await bunload.publish({ collection: "articles", id: draft.doc.id });
const history = await bunload.versions({ collection: "articles", id: draft.doc.id });
await bunload.restoreVersion({ collection: "articles", id: draft.doc.id, version: history.versions[0].version });
await bunload.deleteById({ collection: "articles", id: draft.doc.id });
await bunload.restore({ collection: "articles", id: draft.doc.id });

Upload collections use multipart form data through bunload.upload(). Managed filename, mimeType, and filesize fields are returned; internal storage keys are never exposed. File delivery uses the collection's read access and field visibility through bunload.fileURL(). Soft delete retains the object; permanent delete removes it. S3StorageAdapter uses Bun's native S3 client and supports S3-compatible endpoints.

Custom endpoints use /api/:collection/:path and return a standard Response. Their optional access rule receives the authenticated user and collection context.

Run the core suite from the workspace root with bun run test:core.