12 modules. One pattern. Auto-scaling from in-memory to Redis to cloud — without changing a single line of code.
npm install @bloomneo/appkit
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
authClassJWT login/API tokens, 9-level role hierarchy, Express middleware, password hashingdatabaseClassPrisma client, multi-tenant filtering, per-org databasessecurityClassRate limiting, CSRF protection, AES-256-GCM encryption, input sanitizationerrorClassSemantic HTTP errors (400–500), async route wrapper, centralized error middlewarecacheClassMemory → Redis auto-scaling, getOrSet() pattern, namespace isolationstorageClassLocal disk → S3/R2 auto-scaling, presigned URLs, file listing, copyqueueClassMemory → Redis background jobs, add(), process(), schedule(delayMs)emailClassConsole → SMTP → Resend auto-scaling, sendTemplate(), batch sendeventClassMemory → Redis pub/sub, wildcard patterns, namespace isolationloggerClassStructured logging, multi-transport (console / file / HTTP)configClassType-safe env access, getRequired(), validateRequired()utilClass12 zero-dependency helpers: slugify, uuid, chunk, debounce, clamp, formatBytes, and moreTwo 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.
| Level | Role.Level | Typical use |
|---|---|---|
| 1 | user.basic | Free / read-only tier |
| 2 | user.pro | Paid tier |
| 3 | user.max | Premium tier |
| 4 | moderator.review | Can flag content |
| 5 | moderator.approve | Can publish content |
| 6 | moderator.manage | Can manage moderators |
| 7 | admin.tenant | Tenant administrator |
| 8 | admin.org | Organisation admin |
| 9 | admin.system | Super admin |
requireUserRoles(['admin.tenant']) accepts levels 7, 8, and 9 via inheritance. Add custom roles with BLOOM_AUTH_ROLES=role.level:N,....
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 var | Purpose |
|---|---|
DATABASE_URL | Prisma connection string (required) |
BLOOM_DB_TENANT=auto | Enable automatic multi-tenant filtering |
ORG_<NAME>=postgresql://... | Per-org database URL |
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 var | Purpose |
|---|---|
BLOOM_SECURITY_CSRF_SECRET | CSRF token signing (min 32 chars) |
BLOOM_SECURITY_ENCRYPTION_KEY | AES-256-GCM key (64 hex chars) |
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());
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.
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.
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.
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?);
| Strategy | When active |
|---|---|
console | Default in dev — logs email to terminal |
smtp | SMTP_HOST + SMTP_USER + SMTP_PASS set |
resend | RESEND_API_KEY set |
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.
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 var | Effect |
|---|---|
| (none) | Console transport only (default) |
BLOOM_LOGGER_FILE_PATH=/var/log/app.log | Adds file transport |
BLOOM_LOGGER_HTTP_URL=https://logs.example.com | Adds HTTP transport |
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
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
| Module | Default | Set this env var | Upgrades to |
|---|---|---|---|
| Cache | in-memory | REDIS_URL | Redis |
| Queue | in-memory | REDIS_URL | Redis |
| Events | in-memory | REDIS_URL | Redis pub/sub |
| Storage | local disk | AWS_S3_BUCKET or R2_BUCKET | S3 / Cloudflare R2 |
| console log | SMTP_HOST or RESEND_API_KEY | SMTP / Resend | |
| Logger | console | BLOOM_LOGGER_FILE_PATH | + file transport |
| Variable | Purpose | Required? |
|---|---|---|
BLOOM_AUTH_SECRET | JWT signing key (min 32 chars) | Yes |
DATABASE_URL | Prisma connection string | Yes |
BLOOM_SECURITY_CSRF_SECRET | CSRF token signing | Recommended |
BLOOM_SECURITY_ENCRYPTION_KEY | AES-256-GCM (64 hex chars) | Recommended |
REDIS_URL | Distributed cache + queue + events | Optional |
AWS_S3_BUCKET | Cloud storage bucket | Optional |
RESEND_API_KEY | Production transactional email | Optional |
SMTP_HOST / SMTP_USER / SMTP_PASS | SMTP email | Optional |
BLOOM_DB_TENANT=auto | Enable multi-tenant filtering | Optional |
ORG_<NAME> | Per-org database URL | Optional |
BLOOM_LOGGER_FILE_PATH | Log to file | Optional |
BLOOM_LOGGER_HTTP_URL | Log to HTTP endpoint | Optional |
BLOOM_AUTH_ROLES | Custom role.level definitions | Optional |
// 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.txtFull API reference — every method, every signature. Auto-regenerated on each build. View →
AGENTS.mdRules for AI agents: always-do, never-do, canonical patterns. Prevents hallucinated method names. View →
docs/Bloom CLI postinstall copies the latest llms.txt and AGENTS.md into your project's docs/ on every npm install.