npm install @bloomneo/appkit

For AI coding agents (Claude, Cursor, Copilot, Windsurf)

This page is written for humans. Agents should fetch /appkit/llms.txt and /appkit/AGENTS.md as the authoritative specs — they list every method signature, the canonical pattern, and what NOT to do. Same files ship inside the published npm package under docs/ and are kept in sync on every release.

Copy this into any Node.js file

The minimum to have every module available in a single file. All twelve follow the same xxxClass.get() pattern — no constructors, no factories.

import {
  authClass, databaseClass, securityClass, errorClass,
  cacheClass, storageClass, queueClass, emailClass,
  eventClass, loggerClass, configClass, utilClass,
} from '@bloomneo/appkit';

const auth     = authClass.get();
const database = await databaseClass.get();     // async — opens Prisma connection
const security = securityClass.get();
const error    = errorClass.get();
const cache    = cacheClass.get('users');        // optional namespace
const storage  = storageClass.get();
const queue    = queueClass.get();
const email    = emailClass.get();
const events   = eventClass.get();
const logger   = loggerClass.get('api');         // optional component label
const config   = configClass.get();
const util     = utilClass.get();

One toolkit. Real-world backends.

AppKit is the Node.js backend toolkit teams reach for when they need to ship — not evaluate ten libraries.

One way to instantiate. Always.

Every module follows xxxClass.get(). No constructors, no factories, no special cases. If you're writing new SomethingClass(), you're doing it wrong.

import {
  authClass, databaseClass, errorClass, loggerClass,
  cacheClass, storageClass, queueClass, emailClass,
  eventClass, configClass, securityClass, utilClass,
} from '@bloomneo/appkit';

// Always at module scope — NEVER inside route handlers
const auth     = authClass.get();
const database = await databaseClass.get();   // async — initializes Prisma connection
const error    = errorClass.get();
const logger   = loggerClass.get('api');      // optional component label
const cache    = cacheClass.get('users');     // optional namespace for isolation

The five mistakes AppKit won't let you make twice

These are the only ways to misuse AppKit. Memorise them — or point your AI agent at AGENTS.md where they're spelled out line by line.

JWT tokens, roles, and middleware

Use it when you need
  • JWT authentication for Express or Fastify APIs
  • User signup, login, bcrypt password hashing
  • API keys for webhooks or service-to-service calls
  • Role-based access control (RBAC) for admin routes
  • Tiered pricing enforcement (Free / Pro / Enterprise)
  • Multi-tenant user isolation
  • SSO callback handling, magic-link login, OAuth
  • Refresh-token rotation for long-lived sessions

Two token types: login tokens (humans) and API tokens (services / webhooks). Both verified with the same verifyToken().

const auth = authClass.get();

// Token generation
auth.generateLoginToken(payload, expiresIn?: string): string
//   payload: { userId: number, role: string, level: string, permissions?: string[] }
//   default expiresIn: '7d'

auth.generateApiToken(payload, expiresIn?: string): string
//   payload: { keyId: string, role: string, level: string, permissions?: string[] }
//   default expiresIn: '1y'

// Verification — handles BOTH token types. Throws on invalid token.
auth.verifyToken(token: string): JwtPayload

// Password hashing
auth.hashPassword(plain: string, rounds?: number): Promise<string>
auth.comparePassword(plain: string, hash: string): Promise<boolean>  // never throws

// Express middleware factories
auth.requireLoginToken(options?): ExpressMiddleware        // authenticate user
auth.requireApiToken(options?): ExpressMiddleware          // authenticate service
auth.requireUserRoles(['admin.tenant']): ExpressMiddleware // role check (OR logic)
auth.requireUserPermissions(['manage:users']): ExpressMiddleware  // permission check (AND logic)

// Request helpers
auth.getUser(req): JwtPayload | null   // null-safe extract from req.user
auth.hasRole(userRole, requiredRole): boolean   // inheritance-aware comparison
auth.hasPermission(user, permission): boolean
import { authClass, databaseClass, errorClass } from '@bloomneo/appkit';

const auth     = authClass.get();
const database = await databaseClass.get();
const error    = errorClass.get();

app.post('/auth/login', error.asyncRoute(async (req, res) => {
  const { email, password } = req.body ?? {};
  if (!email || !password) throw error.badRequest('Email and password required');

  const user = await database.user.findUnique({ where: { email } });
  if (!user) throw error.unauthorized('Invalid credentials');

  const valid = await auth.comparePassword(password, user.password);
  if (!valid) throw error.unauthorized('Invalid credentials');

  const token = auth.generateLoginToken({
    userId: user.id,
    role: user.role,
    level: user.level,
  });
  res.json({ token, user: { id: user.id, email: user.email } });
}));
app.delete(
  '/admin/users/:id',
  auth.requireLoginToken(),                    // 1. authenticate — sets req.user
  auth.requireUserRoles(['admin.tenant']),     // 2. authorize — chained AFTER authenticate
  error.asyncRoute(async (req, res) => {
    await database.user.delete({ where: { id: Number(req.params.id) } });
    res.json({ deleted: true });
  })
);

// IMPORTANT: requireUserRoles() always chains AFTER requireLoginToken().
// requireApiToken() is for service routes — never chain requireUserRoles() after it.
LevelRole.LevelTypical use
1user.basicFree / read-only tier
2user.proPaid tier
3user.maxPremium tier
4moderator.reviewCan flag content
5moderator.approveCan publish content
6moderator.manageCan manage moderators
7admin.tenantTenant administrator
8admin.orgOrganisation admin
9admin.systemSuper admin

requireUserRoles(['admin.tenant']) accepts levels 7, 8, and 9 via inheritance. Add custom roles with BLOOM_AUTH_ROLES=role.level:N,....

Multi-tenant Prisma with auto-filtering

Use it when you need
  • B2B SaaS with row-level tenant isolation
  • Multi-tenant PostgreSQL, MySQL, SQLite backends
  • Multi-org deployments with per-customer databases
  • Prisma-powered Node.js APIs without boilerplate
  • Data-residency-sensitive apps (EU / US split)
  • HIPAA-aligned healthcare backends
  • SOC2-aligned customer data isolation
  • Zero-leak guarantee across tenants
const database = await databaseClass.get();

// Standard Prisma queries — auto-filtered by tenant_id when BLOOM_DB_TENANT=auto
database.user.findMany({ where: { active: true } })
database.user.create({ data: { email, name } })   // auto-stamps tenant_id

// Cross-tenant queries (admin only)
const dbAll = await databaseClass.getTenants();
dbAll.user.groupBy({ by: ['tenant_id'], _count: true })

// Per-org databases (multi-org SaaS)
const acmeDb = await databaseClass.org('acme').get();  // reads ORG_ACME env var

Rate limiting, CSRF, encryption

Use it when you need
  • Brute-force protection on login endpoints
  • Per-IP / per-user rate limiting for REST & GraphQL
  • CSRF protection for Express form routes
  • AES-256-GCM encryption for API keys, tokens, PII
  • XSS input sanitization for user-generated content
  • Safe HTML rendering in CMS or comment systems
  • PCI-DSS / SOC2-adjacent security hardening
  • Replay-attack protection for webhook receivers
const security = securityClass.get();

// Rate limiting middleware
app.use(security.requests(100, 900_000));   // 100 req per 15 min per IP

// CSRF — one method that does both injection AND validation on HTML form routes
app.use(security.forms());

// AES-256-GCM encryption
const ciphertext = security.encrypt('secret data');
const plaintext  = security.decrypt(ciphertext);

// Input sanitization (strips XSS — NOT email/URL validation; use zod for those)
const clean    = security.input(userInput);      // strip XSS + control chars
const safeHtml = security.html(userHtml);        // strip disallowed HTML tags
const escaped  = security.escape(userText);      // & < > etc.

Semantic HTTP errors with auto-middleware

Use it when you need
  • Consistent JSON error responses across every endpoint
  • Correct HTTP status codes (400 / 401 / 403 / 404 / 409 / 429 / 500)
  • Clean async routes without try/catch boilerplate
  • Centralised error logging, redacted in production
  • Prisma / Zod / JWT errors mapped to HTTP semantics
  • Structured error bodies for frontend error boundaries
  • One global error middleware for any Express app
const error = errorClass.get();

// Throw semantic errors — each sets the correct HTTP status automatically
throw error.badRequest('validation message')   // 400
throw error.unauthorized('auth failed')        // 401
throw error.forbidden('access denied')         // 403
throw error.notFound('resource missing')       // 404
throw error.conflict('email already exists')   // 409
throw error.tooMany('slow down')               // 429
throw error.internal('unexpected error')       // 500

// Wrap async routes — catches thrown errors and forwards to middleware
app.get('/items/:id', error.asyncRoute(async (req, res) => {
  const item = await database.item.findUnique({ where: { id: +req.params.id } });
  if (!item) throw error.notFound('Item not found');
  res.json(item);
}));

// Mount LAST — handles all semantic errors globally
app.use(error.handleErrors());

Memory → Redis. Zero code changes.

Use it when you need
  • Cache expensive Prisma or Postgres queries
  • Memoize GraphQL resolver results
  • Cache LLM completions (OpenAI, Anthropic, Gemini)
  • Share a warm cache across Node.js processes
  • Throttle third-party API calls (Stripe, Twilio)
  • Deduplicate webhook deliveries
  • Back a session store for Express
  • Upgrade in-memory cache to Redis in production
const cache = cacheClass.get('users');   // namespace for isolation

// The recommended pattern — cache-or-fetch in one call
const users = await cache.getOrSet(
  'all-active',
  () => database.user.findMany({ where: { active: true } }),
  300   // TTL in seconds
);

// Low-level operations
await cache.set('key', value, ttlSeconds?);
const val = await cache.get<User>('key');    // returns null if missing
await cache.delete('key');   // NOTE: delete(), not del()

// Check existence — there is no cache.has(). Use null check:
const v = await cache.get('key');
if (v !== null) { /* key exists */ }

// Invalidate after mutation
await cache.delete('all-active');

Set REDIS_URL to upgrade from in-memory to Redis automatically.

Local disk → S3/R2. Same API.

Use it when you need
  • User file uploads (avatars, documents, video)
  • Stream multipart uploads to S3 or Cloudflare R2
  • Generate + store PDF invoices or CSV exports
  • Private downloads with expiring presigned URLs
  • Back a CMS media library
  • Store AI-generated images (DALL·E, Stable Diffusion)
  • Migrate local disk → S3 / R2 / DigitalOcean Spaces
  • Admin tooling with copy / list / delete operations
const storage = storageClass.get();

// Upload a file
await storage.put('avatars/user-123.jpg', buffer, { contentType: 'image/jpeg' });

// Download, check, delete, list
const buf    = await storage.get('avatars/user-123.jpg');
const exists = await storage.exists('avatars/user-123.jpg');  // NOTE: exists(), not has()
await storage.delete('avatars/user-123.jpg');                 // NOTE: delete(), not del()
const files  = await storage.list('avatars/', 100);

// URLs
const publicUrl  = storage.url('avatars/user-123.jpg');
const signedUrl  = await storage.signedUrl('private/doc.pdf', 3600);  // 1hr expiry
await storage.copy('src/file.jpg', 'dst/file.jpg');

Set AWS_S3_BUCKET (or R2_BUCKET) to upgrade from local disk to cloud storage.

Background jobs with automatic scaling

Use it when you need
  • Send transactional emails in the background
  • Resize or transcode uploaded images & videos
  • Call long-running APIs (OpenAI, Stripe, SendGrid)
  • Generate PDF reports, CSV or Excel exports
  • Fan-out webhook deliveries to subscribers
  • Scheduled cleanup, data archival, retry logic
  • CSV imports, CRM sync, analytics ETL pipelines
  • LLM prompt batching with exponential backoff
const queue = queueClass.get();

// Enqueue a job
await queue.add('send-email', { to: 'user@example.com', template: 'welcome' }, {
  delay: 0,          // ms to wait before running
  attempts: 3,       // max retries (NOTE: "attempts" not "retries")
  priority: 1,       // lower = higher priority
});

// Register a worker — throw to trigger retry
queue.process('send-email', async (data) => {
  await email.sendTemplate('welcome', { to: data.to });
});

// Delay — schedule once N milliseconds from now (NOT a cron expression)
await queue.schedule('cleanup-tokens', {}, 8 * 60 * 60 * 1000);  // 8 hours

// Management
await queue.pause('send-email');
await queue.resume('send-email');
const stats = await queue.getStats('send-email');
await queue.retry(jobId);
await queue.remove(jobId);

In-memory by default. Set REDIS_URL for persistent, distributed queues.

Console → SMTP → Resend. Auto-configured.

Use it when you need
  • Welcome onboarding emails, email verification flows
  • Password reset links, magic-link login
  • Order confirmations, shipping notifications
  • Invoice receipts and billing alerts
  • Weekly digest newsletters, lifecycle drip campaigns
  • Templated emails with variable substitution
  • HTML + plain-text multipart sending
  • Failover between Resend and SMTP providers
const email = emailClass.get();

// Plain or HTML email
await email.send({
  to: 'user@example.com',
  subject: 'Welcome to the platform',
  text: 'Plain-text fallback',
  html: '<p>HTML body</p>',   // send both for best deliverability
  from: 'noreply@yourapp.com',
  replyTo: 'support@yourapp.com',
});

// Convenience shortcuts
await email.sendText(to, subject, text);
await email.sendHtml(to, subject, html, text?);

// Template email — all data in one object
await email.sendTemplate('password-reset', {
  to: user.email,
  resetLink: 'https://app.com/reset?token=abc',
  expiresIn: '1 hour',
});

// Batch
await email.sendBatch(emailsArray, batchSize?);

Default is console in dev. Set RESEND_API_KEY (or SMTP_HOST + SMTP_USER + SMTP_PASS) to switch transports automatically.

Pub/Sub with wildcard patterns

Use it when you need
  • Decouple features: one event, many independent reactors
  • Broadcast across Node.js processes via Redis pub/sub
  • Domain-event bus or lightweight event sourcing
  • Real-time notifications (SSE, WebSockets)
  • Activity feeds, audit trails, inbox notifications
  • Downstream webhook delivery triggers
  • Monolith → microservices migration path
  • Avoid pulling in Kafka or RabbitMQ for small workloads
const events = eventClass.get();
const userEvents = eventClass.get('users');   // namespace isolation

// Subscribe
events.on('user.created', async (data) => {
  await email.sendTemplate('welcome', { to: data.email });
});

events.on('user.*', (data) => {   // wildcard pattern
  logger.info('User event', data);
});

// Emit
await events.emit('user.created', { id: 123, email: 'new@example.com' });

// Unsubscribe
events.off('user.created', handler);

Set REDIS_URL to distribute events across multiple processes or servers via Redis pub/sub.

Structured logging with multi-transport

Use it when you need
  • Structured JSON logs for Datadog, Loki, CloudWatch
  • Ship to Papertrail, Logtail, Axiom, Honeycomb
  • Component-tagged logs (filter by service)
  • Correlation IDs across request and worker
  • Multi-level logging (debug / info / warn / error / fatal)
  • Log-to-file transport for audit compliance
  • HTTP shipping without running a sidecar agent
  • Automatic redaction of secrets and PII
const logger = loggerClass.get('api');   // component-tagged

logger.debug('Processing request', { path: req.path });
logger.info('User created', { userId: newUser.id });
logger.warn('Rate limit approaching', { ip: req.ip, count: 95 });
logger.error('Database query failed', { error: err.message });
logger.fatal('Service unresponsive', { service: 'redis' });

Console transport by default. Set BLOOM_LOGGER_FILE_PATH for file transport or BLOOM_LOGGER_HTTP_URL for HTTP shipping.

Type-safe environment variables

Use it when you need
  • Single source of truth for environment config
  • Fail-fast validation on boot for missing secrets
  • Typed accessors for URLs, API keys, feature flags
  • NODE_ENV-aware dev / staging / prod / test branches
  • Dotenv-compatible loading
  • Keep process.env out of business logic
  • Testable config — inject different values per test
const config = configClass.get();

// Read values
config.get('auth.secret', defaultValue?)          // always returns string | undefined
config.getRequired('auth.secret')                 // throws if missing
config.has('auth.secret'): boolean
config.getMany(['KEY_A', 'KEY_B']): Record<string, string | undefined>

// NOTE: getNumber() and getBoolean() don't exist. Parse manually:
const port  = Number(config.get('api.port') ?? 3000);
const debug = config.get('app.debug') === 'true';

// Static helpers — on configClass directly (not on the instance)
configClass.isDevelopment(): boolean
configClass.isProduction(): boolean
configClass.isTest(): boolean
configClass.validateRequired(['BLOOM_AUTH_SECRET', 'DATABASE_URL'])  // throws if any missing

12 zero-dependency helpers

Use it when you need
  • URL-safe slugs for blog posts and CMS routes
  • UUID v4 for request IDs and entity keys
  • Chunked batch processing for bulk inserts / API calls
  • Debounced input handlers
  • Clamp / safe-get / deep-pick helpers
  • Human-readable byte formatting (1.5 MB)
  • Text truncation with ellipsis
  • Replace lodash, ramda, nanoid for one-off helpers
const util = utilClass.get();

util.get(obj, 'a.b.c', defaultValue)  // safe deep property read (no util.set())
util.pick(obj, ['a', 'b'])            // keep listed keys   (no util.omit())
util.isEmpty(value): boolean          // true for null, undefined, '', [], {}
util.chunk(array, 5)                  // split into batches of 5
util.unique(array)                    // deduplicate
util.clamp(150, 0, 100)              // → 100
util.debounce(fn, 300)               // (no util.throttle())
util.sleep(1000)                     // Promise-based delay
util.uuid()                          // v4 UUID
util.slugify('Hello World')          // → 'hello-world'
util.formatBytes(1_572_864)          // → '1.5 MB'
util.truncate(longText, 80)          // truncate with ellipsis
ModuleDefaultSet this env varUpgrades to
Cachein-memoryREDIS_URLRedis
Queuein-memoryREDIS_URLRedis
Eventsin-memoryREDIS_URLRedis pub/sub
Storagelocal diskAWS_S3_BUCKET or R2_BUCKETS3 / Cloudflare R2
Emailconsole logSMTP_HOST or RESEND_API_KEYSMTP / Resend
LoggerconsoleBLOOM_LOGGER_FILE_PATH+ file transport
VariablePurposeRequired?
BLOOM_AUTH_SECRETJWT signing key (min 32 chars)Yes
DATABASE_URLPrisma connection stringYes
BLOOM_SECURITY_CSRF_SECRETCSRF token signingRecommended
BLOOM_SECURITY_ENCRYPTION_KEYAES-256-GCM (64 hex chars)Recommended
REDIS_URLDistributed cache + queue + eventsOptional
AWS_S3_BUCKETCloud storage bucketOptional
RESEND_API_KEYProduction transactional emailOptional
SMTP_HOST / SMTP_USER / SMTP_PASSSMTP emailOptional
BLOOM_DB_TENANT=autoEnable multi-tenant filteringOptional
ORG_<NAME>Per-org database URLOptional
BLOOM_LOGGER_FILE_PATHLog to fileOptional
BLOOM_LOGGER_HTTP_URLLog to HTTP endpointOptional
BLOOM_AUTH_ROLESCustom role.level definitionsOptional
// Preferred — flat import when using multiple modules
import { authClass, databaseClass, errorClass, cacheClass } from '@bloomneo/appkit';

// Alternative — subpath for tree-shaking when only one module is needed
import { authClass }     from '@bloomneo/appkit/auth';
import { databaseClass } from '@bloomneo/appkit/database';
import { securityClass } from '@bloomneo/appkit/security';
import { errorClass }    from '@bloomneo/appkit/error';
import { cacheClass }    from '@bloomneo/appkit/cache';
import { storageClass }  from '@bloomneo/appkit/storage';
import { queueClass }    from '@bloomneo/appkit/queue';
import { emailClass }    from '@bloomneo/appkit/email';
import { eventClass }    from '@bloomneo/appkit/event';
import { loggerClass }   from '@bloomneo/appkit/logger';
import { configClass }   from '@bloomneo/appkit/config';
import { utilClass }     from '@bloomneo/appkit/util';

~80% of AppKit endpoints follow this shape. Generate this as the default unless told otherwise.

import { authClass, databaseClass, errorClass, loggerClass } from '@bloomneo/appkit';

const auth     = authClass.get();
const database = await databaseClass.get();
const error    = errorClass.get();
const logger   = loggerClass.get('users');

app.post(
  '/api/users',
  auth.requireLoginToken(),                   // 1. authenticate
  auth.requireUserRoles(['admin.tenant']),    // 2. authorize (always chained after authenticate)
  error.asyncRoute(async (req, res) => {
    if (!req.body?.email) throw error.badRequest('Email required');

    const user = await database.user.create({ data: req.body });
    logger.info('User created', { userId: user.id });
    res.json({ user });
  })
);

// Mount once, last in the stack
app.use(error.handleErrors());

Frequently asked questions

What is @bloomneo/appkit?
@bloomneo/appkit is a Node.js backend framework designed for agentic code workflows — the era where developers and AI coding agents (Claude Code, Cursor, GitHub Copilot, Windsurf) build backends together. It ships twelve typed modules — auth, database, cache, queue, storage, email, events, logging, config, security, error, util — under one canonical pattern (xxxClass.get()) and includes llms.txt and AGENTS.md machine-readable specs in every release so agents generate correct code the first time.
How is AppKit different from NestJS, AdonisJS, or Fastify directly?
NestJS and AdonisJS are opinionated frameworks that replace Express. AppKit is the opposite: it's a toolkit you drop into an existing Express or Fastify app (or a bare Node.js script). You pick your HTTP framework; AppKit provides the twelve infrastructure modules every production backend needs, under one consistent pattern. Think of it as "the batteries Express was missing" rather than a framework lock-in.
Does AppKit work with Fastify?
Yes. The middleware factories (auth.requireLoginToken(), error.asyncRoute(), security.requests()) are compatible with both Express and Fastify. Non-HTTP modules (cache, queue, storage, email, events, logger, config, util, database) are framework-agnostic and work in any Node.js process — HTTP server, worker, CLI, or cron job.
How do I use AppKit with Claude Code, Cursor, or GitHub Copilot?
Two options. (1) Point your agent at https://dev.bloomneo.com/appkit/llms.txt and /appkit/AGENTS.md — these are authoritative, machine-readable specs. (2) Scaffold with @bloomneo/bloom, which copies the latest llms.txt and AGENTS.md into your project's docs/ folder on install. Most agents pick them up automatically from that location.
Does it support TypeScript?
AppKit is written in TypeScript and ships full type definitions for all twelve modules. Every method signature, payload shape, and env var is typed. JavaScript consumers also work — types simply aren't enforced.
How does auto-scaling work?
Cache, queue, events, storage, email, and logger start with in-process defaults (in-memory map, local disk, console output). Set one environment variable — REDIS_URL, AWS_S3_BUCKET, RESEND_API_KEY, BLOOM_LOGGER_FILE_PATH — and the same module transparently switches to the production backend. No refactor, no new library, no code change.
Can I use AppKit for multi-tenant SaaS?
Yes — that's a primary use case. Set BLOOM_DB_TENANT=auto and every Prisma query is automatically scoped to the current tenant via row-level filtering. For multi-org deployments where each customer has its own database, use databaseClass.org('acme').get() backed by per-org connection strings (ORG_ACME=postgresql://…).
What database does AppKit use?
The database module wraps Prisma, so you can use PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, or CockroachDB — anything Prisma supports. Your schema.prisma stays yours; AppKit adds multi-tenant filtering and per-org connection handling on top.
Is AppKit production-ready?
Yes. AppKit v4.0.0 is stable, semver-tracked, and designed for production. All twelve modules ship with structured error handling, graceful degradation, and sensible defaults. Required env vars (BLOOM_AUTH_SECRET, DATABASE_URL, BLOOM_SECURITY_CSRF_SECRET) are validated on boot so missing secrets crash early, not at 3am.
What license is AppKit under?
MIT. Use it in commercial projects, fork it, vendor it — no attribution required. The source lives on GitHub at github.com/bloomneo/appkit.

llms.txt

Full API reference — every method, every signature. Auto-regenerated on each build. View →

AGENTS.md

Rules for AI agents: always-do, never-do, canonical patterns. Prevents hallucinated method names. View →

Auto-synced docs/

Bloom CLI postinstall copies the latest llms.txt and AGENTS.md into your project's docs/ on every npm install.