| name | zintrust-development |
| description | ZinTrust repository development conventions, runtime rules, and architecture guidance. |
ZinTrust Development Skill
This skill provides AI agents with comprehensive knowledge of ZinTrust's conventions, patterns, and architecture so they can write correct, idiomatic code for the framework.
1. The Golden Rules (Always Check First)
- Dual-runtime mandate: Every change must work with
zin s (Node.js) AND zin s --wg (Cloudflare Workers). If anything is runtime-specific, flag it before implementing.
- Brand spelling: Always "ZinTrust" — never "zintrust", "Zintrust", or "ZINTrust" in prose, docs, and comments. Code identifiers (type names, module paths, etc.) must match the actual exported names even when those names use "Zintrust" (e.g.
IZintrustError, @exceptions/ZintrustError).
- No classes: Use factory functions and sealed namespaces (
Object.freeze) in src/, app/, routes/, bin/.
- No
new Error(...): Use ErrorFactory.* from @exceptions/ZintrustError in all app/framework code. CLI commands (src/cli/) and low-level core internals may use new Error where lint overrides are present.
- No
console.*: Use Logger.* from @config/logger in app/framework code. CLI commands and scripts may use console.* where appropriate.
- No raw Node built-ins: Import through
@node-singletons/* wrappers (e.g. import { join } from '@node-singletons/path') in runtime-agnostic app/framework code. CLI commands (src/cli/) and scripts may import node:* directly.
- Use shared helpers: Use utilities from
@helper/index instead of re-implementing them.
- Approved optional lazy-load pattern only: For optional
@zintrust/* startup/runtime imports that must work in both Node and Workers, use the two-step pattern only: first probe with await import('@zintrust/pkg/...') via a tryImport(...) helper, then retry with await import('@zintrust/' + 'pkg/...').catch(() => undefined) if needed. Do not use ??= fallback chains or alternate lazy-load approaches unless explicitly directed.
- Consistency over cleverness: When in doubt, copy structure from an existing similar module.
2. Project Layout
app/ # Application code (Controllers, Jobs, Middleware, Models, Workers)
Controllers/ # Request handlers (factory pattern, never classes)
Jobs/ # Background job services
Middleware/ # App-level middleware
Models/ # ORM model definitions
Workers/ # Background worker definitions
bin/ # CLI entrypoints (zin.ts, z.ts)
config/ # Runtime config (database.ts, cache.ts, queue.ts, mail.ts, workers.ts)
database/ # Migrations, seeders, factories
@zintrust/* # Optional adapters (db, cache, queue, mail, storage)
routes/ # Route definitions (api.ts)
src/ # Framework core (DO NOT modify unless contributing to core)
auth/ # Auth subsystem
cache/ # Cache driver abstraction
cli/ # CLI infrastructure + all commands
config/ # env.ts, logger.ts, security.ts, cloudflare.ts
exceptions/ # ZintrustError, ErrorFactory
helper/ # Shared type/validation helpers
http/ # Request and Response abstractions
middleware/ # Framework middleware
microservices/ # Microservice framework
migrations/ # Migration engine
node-singletons/ # Node built-in wrappers (for cross-runtime compat)
orm/ # ORM: Model, QueryBuilder, relations
routes/ # Router implementation
security/ # JWT, CSRF, Encryption, Hash, Sanitizer, XSS, etc.
services/ # Framework services
session/ # Session management
start.ts # Bootstrap entry
validation/ # Schema + Validator
tests/ # All tests (unit/, integration/, patch-coverage/)
vitest.setup.ts # Global Vitest setup and mocks
3. Path Aliases (tsconfig.json)
Always use alias imports in framework and app code. Never use relative paths like ../../src.
'@zintrust/core' → src/index.ts
'@/*' → src
4. Routing (routes/api.ts)
import { type IRouter, Router } from '@core-routes/Router';
import type { MiddlewareKey } from '@config/middleware';
export function registerRoutes(router: IRouter): void {
Router.get(router, '/health', healthHandler);
Router.post(router, '/login', loginHandler);
Router.get<MiddlewareKey>(router, '/profile', profileHandler, {
middleware: ['auth', 'jwt'],
});
Router.group(router, '/api/v1', (r: IRouter) => {
Router.get(r, '/users', usersHandler);
Router.post(r, '/users', createUserHandler);
});
Router.group<MiddlewareKey>(
router,
'/admin',
(r: IRouter) => {
Router.get(r, '/dashboard', dashboardHandler);
},
{ middleware: ['auth'] }
);
const ctrl = UserController.create();
Router.resource<MiddlewareKey>(
router,
'/users',
{
index: ctrl.index,
store: ctrl.store,
show: ctrl.show,
update: ctrl.update,
destroy: ctrl.destroy,
},
{
middleware: ['auth'],
store: { middleware: ['auth', 'validateUserStore'] },
update: { middleware: ['auth', 'validateUserUpdate'] },
}
);
}
Route param syntax:
:id → single path segment
:path* → greedy (captures slashes too)
5. Controllers / Request Handlers
Use the factory + sealed namespace pattern. Never use classes.
import type { IRequest } from '@http/Request';
import type { IResponse } from '@http/Response';
import { ErrorFactory } from '@exceptions/ZintrustError';
import { Logger } from '@config/logger';
import { requireValidatedBody } from '@http/ValidationHelper';
import { User } from '@app/Models/User';
async function index(req: IRequest, res: IResponse): Promise<void> {
const users = await User.query().all();
res.json({ data: users });
}
async function show(req: IRequest, res: IResponse): Promise<void> {
const id = req.getParam('id');
const user = await User.query().find(Number(id));
if (!user) {
throw ErrorFactory.createNotFoundError('User not found', { id });
}
res.json({ data: user });
}
async function store(req: IRequest, res: IResponse): Promise<void> {
const body = requireValidatedBody<{ name: string; email: string }>(req);
const user = await User.create(body);
Logger.info('User created', { id: user.getAttribute('id') });
res.setStatus(201).json({ data: user });
}
export const UserController = Object.freeze({
create: () => Object.freeze({ index, show, store }),
});
Useful request/response methods:
req.getParam('id');
req.getQuery('page');
getValidatedBody<T>(req);
requireValidatedBody<T>(req);
hasValidatedBody(req);
req.getRaw();
res.json({ key: 'value' });
res.setStatus(201).json({});
res.setHeader('X-Header', 'v');
res.locals.myData = value;
6. ORM Models
import { Model } from '@orm/Model';
export const User = Model.define({
table: 'users',
fillable: ['name', 'email', 'password'],
hidden: ['password'],
timestamps: true,
softDeletes: true,
casts: {
id: 'number',
active: 'boolean',
created_at: 'date',
},
accessors: {
full_name: (value, attrs) => `${attrs.first_name} ${attrs.last_name}`,
},
mutators: {
password: (value) => hashSync(value),
},
scopes: {
active: (builder) => builder.where('active', true),
},
});
Model instance methods:
model.fill(attributes)
model.setAttribute('key', value)
model.getAttribute('key')
model.getAttributes()
model.save()
model.delete()
model.restore()
model.forceDelete()
model.isDeleted()
model.isDirty('key'?)
model.toJSON()
model.setRelation('posts', posts)
model.getRelation<Post[]>('posts')
Relationship helpers (call on model instance):
model.hasOne(RelatedModel, foreignKey?)
model.hasMany(RelatedModel, foreignKey?)
model.belongsTo(RelatedModel, foreignKey?)
model.belongsToMany(RelatedModel, throughTable?, foreignKey?, relatedKey?)
model.morphOne(RelatedModel, morphName)
model.morphMany(RelatedModel, morphName)
model.morphTo(morphName, morphMap)
model.hasOneThrough(RelatedModel, ThroughModel, firstKey?, secondKey?)
model.hasManyThrough(RelatedModel, ThroughModel, firstKey?, secondKey?)
7. Validation
import { Schema, Validator } from '@validation/Validator';
const schema = Schema.create()
.required('email')
.email('email')
.required('name')
.string('name')
.minLength('name', 2)
.maxLength('name', 255)
.required('age')
.integer('age')
.min('age', 18)
.max('age', 100)
.required('role')
.in('role', ['admin', 'user', 'guest'])
.boolean('active')
.uuid('userId')
.url('website')
.phone('mobile')
.date('dob')
.regex('code', /^\d{4}$/)
.custom('field', (value) => typeof value === 'string');
Register schema as middleware on a route to auto-validate and populate getValidatedBody(). Validation middlewares are concrete named keys registered in src/config/middleware.ts (e.g. validateLogin, validateUserStore). Add new ones there following the same pattern:
Router.post<MiddlewareKey>(router, '/users', handler, {
middleware: ['auth', 'validateUserStore'],
});
8. Error Handling
Import: import { ErrorFactory } from '@exceptions/ZintrustError';
| Method | HTTP Status | Use case |
|---|
ErrorFactory.createNotFoundError(msg?, details?) | 404 | Resource not found |
ErrorFactory.createUnauthorizedError(msg?, details?) | 401 | Not authenticated |
ErrorFactory.createForbiddenError(msg?, details?) | 403 | Authenticated but not allowed |
ErrorFactory.createValidationError(msg, details?) | 400 | Input validation failed |
ErrorFactory.createDatabaseError(msg, details?) | 500 | Database query failure |
ErrorFactory.createConfigError(msg, details?) | 500 | Missing/invalid config |
ErrorFactory.createConnectionError(msg, details?) | 500 | Network/connection failure |
ErrorFactory.createSecurityError(msg, details?) | 401 | Security check failure |
ErrorFactory.createGeneralError(msg, details?) | 500 | Catch-all server error |
ErrorFactory.createWorkerError(msg, details?) | 500 | Worker failure |
ErrorFactory.createSanitizerError(method, reason, value) | 400 | Sanitizer failure |
Usage:
throw ErrorFactory.createNotFoundError('User not found', { id });
throw ErrorFactory.createValidationError('Invalid input', { field: 'email' });
throw ErrorFactory.createForbiddenError();
9. Logging
Import: import { Logger } from '@config/logger';
Logger.debug('message', { extra: 'data' });
Logger.info('User logged in', { userId: 1 });
Logger.warn('Rate limit approaching', { requests: 950 });
Logger.error('Database query failed', error);
Logger.fatal('Unrecoverable error', error);
const log = Logger.scope('AuthController');
log.info('Login attempt', { email });
Auto-redacted fields (never logged in plain text):
password, token, authorization, secret, apikey, api_key, jwt, bearer
Env vars:
LOG_LEVEL → debug | info | warn | error (default: info)
LOG_FORMAT → text | json (default: text)
DISABLE_LOGGING → true to suppress all output
10. Environment / Config
Import: import { Env } from '@config/env';
Env.get('KEY', 'defaultString')
Env.getInt('PORT', 3000)
Env.getFloat('RATE', 1.5)
Env.getBool('FEATURE_FLAG', false)
Env.NODE_ENV Env.PORT Env.HOST
Env.APP_NAME Env.APP_KEY Env.BASE_URL
Env.DB_CONNECTION Env.DB_HOST Env.DB_DATABASE
Env.LOG_LEVEL
11. Helper Utilities
Import: import { isNonEmptyString, isInt, isUUID, ... } from '@helper/index';
Type checks
isString(v) isArray(v) isObject(v)
isFunction(v) isDate(v) isNonEmptyArray(v)
isNonEmptyObject(v)
Null / empty checks
isNonEmptyString(v);
isUndefined(v);
isNull(v);
isUndefinedOrNull(v);
isEmpty(v);
Numeric
isInt(v)
isInt(v, true)
isInt(v, true, { min: 1, max: 100 })
isFloat(v)
isFloat(v, true)
isFloat(v, true, { min: 0.0, max: 1.0 })
isNumeric(v)
isIntString(v, { min?, max? })
isFloatString(v, { min?, max? })
Boolean
isBoolean(v);
isBoolean(v, true);
isBooleanString(v);
String format
isNonEmptyString(v)
isEmail(v) isUrl(v) isUUID(v)
isAlpha(v) isAlphanumeric(v) isSlug(v)
isHexColor(v) isJSON(v) isBase64(v)
isMatch(v, regex)
isWhitespaceOnly(v)
Collections
isIn(v, array) isNotIn(v, array)
isLength(v, n) isMinLength(v, n) isMaxLength(v, n)
⚠️ Common semantics traps
isEmpty(0);
isEmpty('0');
isNull('');
isNull('null');
12. Middleware
import type { Middleware } from '@middleware/MiddlewareStack';
import { Logger } from '@config/logger';
export const MyMiddleware: Middleware = async (req, res, next) => {
Logger.debug('Request received', { path: req.getPath?.() });
await next();
Logger.debug('Request handled');
};
Register your middleware key in config/middleware.ts so it can be referenced by name in route metadata.
13. Node Singletons (Cross-Runtime Wrappers)
Never import node:* directly. Always use the singleton wrappers so code runs in both Node and Cloudflare Workers.
import { join, dirname } from '@node-singletons/path';
import { randomBytes, createHash } from '@node-singletons/crypto';
import { readFile, writeFile } from '@node-singletons/fs';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
Available singletons:
async_hooks, child-process, crypto, events, fs, http, module, net, os, path, perf-hooks, process, readline, stream, tls, url, util
14. Sealed Namespace Pattern
This is the standard for all ZinTrust modules. No classes — use private functions exposed via Object.freeze.
const fetchUser = async (id: number) => {
};
const createUser = async (data: UserInput) => {
};
export const MyService = Object.freeze({
fetchUser,
createUser,
});
export default MyService;
Reference implementations to copy from:
src/routes/Router.ts — routing namespace
src/config/logger.ts — logger namespace
src/orm/Model.ts — ORM namespace
app/Jobs/EmailJobService.ts — job service
15. Jobs / Background Workers
import { MyQueue } from '@app/Workers/MyWorker';
import { Logger } from '@config/logger';
import { ErrorFactory } from '@exceptions/ZintrustError';
export const MyJobService = Object.freeze({
async dispatchEmail(to: string, subject: string, queueName = 'default'): Promise<string> {
const payload = { to, subject, template: 'welcome' };
const jobId = await MyQueue.add(payload, queueName);
Logger.info('Job dispatched', { jobId, to });
return jobId;
},
});
16. Security Features
All security utilities live in src/security/ and are exported via @zintrust/core or @security/*.
| Feature | Import |
|---|
| JWT sign/verify | import { JwtManager } from '@security/JwtManager' |
| JWT sessions (allowlist) | import { JwtSessions } from '@security/JwtSessions' |
| JWT token revocation | import { TokenRevocation } from '@security/TokenRevocation' |
| CSRF protection | import { CsrfTokenManager } from '@security/CsrfTokenManager' |
| Password hashing | import { Hash } from '@security/Hash' |
| AES encryption | import { Encryptor } from '@security/Encryptor' |
| Input sanitizer | import { Sanitizer } from '@security/Sanitizer' |
| XSS protection | import { XssProtection } from '@security/XssProtection' |
| HMAC signed requests | import { SignedRequest } from '@security/SignedRequest' |
| Nonce replay prevention | import { NonceReplay } from '@security/NonceReplay' |
JWT Revocation Drivers (via JWT_REVOCATION_DRIVER env var):
database (default) | redis | kv | kv-remote | memory
17. Available Adapter Packages
| Package | Import |
|---|
| MySQL | @zintrust/db-mysql |
| PostgreSQL | @zintrust/db-postgres |
| SQLite | @zintrust/db-sqlite |
| Cloudflare D1 | @zintrust/db-d1 |
| SQL Server | @zintrust/db-sqlserver |
| Redis Cache | @zintrust/cache-redis |
| Redis Queue | @zintrust/queue-redis |
| RabbitMQ Queue | @zintrust/queue-rabbitmq |
| AWS SQS Queue | @zintrust/queue-sqs |
| Nodemailer Mail | @zintrust/mail-nodemailer |
| Mailgun Mail | @zintrust/mail-mailgun |
| SendGrid Mail | @zintrust/mail-sendgrid |
| AWS S3 Storage | @zintrust/storage-s3 |
| Cloudflare R2 Storage | @zintrust/storage-r2 |
| Google Cloud Storage | @zintrust/storage-gcs |
| Background Workers | @zintrust/workers |
18. Testing Conventions
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { MyModule } from '../../../src/index';
vi.mock('@config/logger', () => ({
Logger: {
info: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
scope: vi.fn(() => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn() })),
},
}));
describe('MyModule', () => {
beforeAll(async () => {
});
afterAll(async () => {
});
it('does the thing correctly', async () => {
await expect(MyModule.doThing('input')).resolves.toBe('expected');
});
it('throws on invalid input', async () => {
await expect(MyModule.doThing('')).rejects.toThrow(/KV binding/);
});
});
Test structure conventions:
- Core/internal module tests import from
src/index (local build); consumer/package tests that test integration with the published package use @zintrust/core (see tests/vitest.setup.ts for its mock)
- Always mock
@config/logger to suppress noise
- Always restore
process.env vars changed in tests
- Use
vi.resetModules() before re-importing a module with different env config
- For env-dependent module tests: set env →
vi.resetModules() → re-import → test → restore env
19. QA Workflow
Always run QA before finishing any task:
npm test
npm run type-check
npm run lint
npm run -s coverage:patch
zin qa
Target order for quick validation:
npm test → fix any broken tests
npm run type-check → fix any type errors
npm run lint → fix any lint warnings
npm run -s coverage:patch → ensure new code is covered
20. CLI Reference
zin s
zin s --wg
zin routes
zin migrate
zin migrate:rollback
zin db:seed
zin key:generate
zin make:controller Name
zin make:model Name
zin make:migration Name
zin make:middleware Name
zin make:job Name
zin schedule:run
zin schedule:start
zin queue
zin debug
zin qa
zin deploy
21. Common Patterns Checklist
When implementing any new feature, confirm: