npm install @bloomneo/appkit

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

JWT tokens, roles, and middleware

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.user(req): JwtPayload | null   // null-safe extract from req.user
auth.hasRole(userRole, requiredRole): boolean   // inheritance-aware comparison
auth.can(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

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
Env varPurpose
DATABASE_URLPrisma connection string (required)
BLOOM_DB_TENANT=autoEnable automatic multi-tenant filtering
ORG_<NAME>=postgresql://...Per-org database URL

Rate limiting, CSRF, encryption

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.
Env varPurpose
BLOOM_SECURITY_CSRF_SECRETCSRF token signing (min 32 chars)
BLOOM_SECURITY_ENCRYPTION_KEYAES-256-GCM key (64 hex chars)

Semantic HTTP errors with auto-middleware

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.

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.

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

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.

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?);
StrategyWhen active
consoleDefault in dev — logs email to terminal
smtpSMTP_HOST + SMTP_USER + SMTP_PASS set
resendRESEND_API_KEY set

Pub/Sub with wildcard patterns

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

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' });
Env varEffect
(none)Console transport only (default)
BLOOM_LOGGER_FILE_PATH=/var/log/app.logAdds file transport
BLOOM_LOGGER_HTTP_URL=https://logs.example.comAdds HTTP transport

Type-safe environment variables

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

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());

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.