Demo: Tasks App (A–Z)
This is a single happy-path demo that exercises ZinTrust end-to-end. The goal: touch the real stuff (DBs, adapters, jobs, templates) without turning it into a novel.
- User register → login
- Create/list/update/complete tasks
- Multi-database setup (auth DB vs tasks DB; plus a demo-only dual SQLite setup)
- Mail, storage, queue, cache, notifications, templates, logging, HTTP client
- Adapter packages (
@zintrust/*) and theirregisterentrypoints - Ecommerce services under
src/services/*(where runnable)
All framework imports in this demo are from:
@zintrust/core(runtime/public API)@zintrust/core/node(Node-only helpers)
If you catch yourself importing deep paths from inside the monorepo, you’re probably doing it the hard way.
0) Prerequisites
- Node.js >= 20
npm i
Optional local infra (recommended for the “all” run):
- Docker (for Postgres/Redis)
Bring up the repo’s local infra (Postgres + optional Redis profile):
npm run docker:up
# Optional: include Redis if you use docker compose profiles
# docker-compose --profile optional up -dQuick sanity (optional):
npm test1) Environment variables (multi DB)
This demo uses named database connections:
auth→ SQLite (users)tasks→ Postgres (tasks)
Demo-only (to prove “multiple connections”):
register→ SQLitelogin→ SQLite
Example .env:
# Server defaults (scaffolded)
HOST=localhost
PORT=7777
LOG_LEVEL=debug
# Auth DB (SQLite)
AUTH_DB_PATH=./tmp/auth.sqlite
# Tasks DB (Postgres)
TASKS_DB_HOST=127.0.0.1
TASKS_DB_PORT=5432
TASKS_DB_DATABASE=zintrust_tasks
TASKS_DB_USERNAME=postgres
TASKS_DB_PASSWORD=postgres
# Demo-only: dual sqlite
REGISTER_DB_PATH=./tmp/register.sqlite
LOGIN_DB_PATH=./tmp/login.sqlite
# Optional features
REDIS_URL=redis://localhost:63792) Bootstrap databases (named connections)
ZinTrust supports multi-DB via named ORM instances. You initialize each connection with useDatabase(config, name).
Important:
- Create/connect named DBs before importing/using models that reference them.
- Each named DB must be connected during bootstrap.
import { Env, useDatabase, type DatabaseConfig } from '@zintrust/core';
export async function initDatabases(): Promise\<void> {
// auth DB (SQLite)
const authCfg: DatabaseConfig = {
driver: 'sqlite',
database: Env.get('AUTH_DB_PATH', './tmp/auth.sqlite'),
};
await useDatabase(authCfg, 'auth').connect();
// tasks DB (Postgres)
const tasksCfg: DatabaseConfig = {
driver: 'postgresql',
host: Env.get('TASKS_DB_HOST', '127.0.0.1'),
port: Env.getInt('TASKS_DB_PORT', 5432),
database: Env.get('TASKS_DB_DATABASE', 'zintrust_tasks'),
username: Env.get('TASKS_DB_USERNAME', 'postgres'),
password: Env.get('TASKS_DB_PASSWORD', 'postgres'),
};
await useDatabase(tasksCfg, 'tasks').connect();
// Demo-only: two sqlite connections
await useDatabase(
{ driver: 'sqlite', database: Env.get('REGISTER_DB_PATH', './tmp/register.sqlite') },
'register'
).connect();
await useDatabase(
{ driver: 'sqlite', database: Env.get('LOGIN_DB_PATH', './tmp/login.sqlite') },
'login'
).connect();
}✅ Expected:
- App boots with all DB connections established.
- Auth queries go to SQLite; tasks queries go to Postgres.
3) Models (auth DB vs tasks DB)
Model definitions should point at the correct named connection.
import { Model } from '@zintrust/core';
export const User = Model.define(
{
table: 'users',
connection: 'auth',
fillable: ['email', 'password_hash'],
hidden: ['password_hash'],
timestamps: true,
casts: {},
},
{}
);
export const Task = Model.define(
{
table: 'tasks',
connection: 'tasks',
fillable: ['user_id', 'title', 'completed'],
hidden: [],
timestamps: true,
casts: { completed: 'boolean' },
},
{}
);✅ Expected:
- Creating a user only touches the SQLite DB.
- Creating a task only touches the Postgres DB.
4) Auth: register + login
Register
import { ErrorFactory, generateUuid } from '@zintrust/core';
export async function register(email: string, passwordHash: string) {
if (email.trim() === '') throw ErrorFactory.createValidationError('Email required');
// Insert into User model (auth DB)
const user = await User.create({
email,
password_hash: passwordHash,
});
// Demo-only: override the model connection to prove separation
// Option A: chain-based override
await User.db('register').create({
id: generateUuid(),
email,
password_hash: passwordHash,
});
return user;
}Login
import { ErrorFactory } from '@zintrust/core';
export async function login(email: string, password: string) {
if (email.trim() === '') throw ErrorFactory.createValidationError('Email required');
// Lookup in User model (auth DB)
const user = await User.query().where('email', email).limit(1).first();
if (!user) throw ErrorFactory.createAuthError('Invalid credentials');
// Compare password (pseudo-code)
// if (!verify(password, user.password_hash)) throw ...
// Demo-only: try to read from 'login' DB
// This proves 'login' DB is distinct from 'register' DB
const replicaUser = await User.db('login').query().where('email', email).limit(1).first();
// console.log('Replica user found:', replicaUser.length > 0);
return { token: 'demo-token', user };
}Raw SQL warning (when you must)
Avoid raw SQL in application code whenever possible.
If you must run raw SQL, ZinTrust protects you by default: raw queries are disabled unless you enable them.
USE_RAW_QRY=true✅ Expected:
Raw queries throw unless
USE_RAW_QRY=true.Always use parameterized queries to avoid injection.
/registercreates a user inauthDB (andregisterDB)./loginfinds user inauthDB (but might miss inloginDB if not synced).
5) Tasks CRUD (tasks DB)
Routes
import { Router, type IRouter } from '@zintrust/core';
import { TasksController } from './TasksController';
export function registerRoutes(router: IRouter) {
Router.group(router, '/api/v1', (r) => {
Router.post(r, '/register', (ctx) => register(ctx.body.email, ctx.body.password));
Router.post(r, '/login', (ctx) => login(ctx.body.email, ctx.body.password));
Router.resource(r, '/tasks', TasksController);
});
}Tasks Controller
import { Cache, Queue, Notification, HttpClient, Logger } from '@zintrust/core';
export const TasksController = {
async index(ctx: any) {
// Cache the task list for 60s
return Cache.remember(`tasks_user_${ctx.user.id}`, 60, async () => {
return Task.findAll({ where: { user_id: ctx.user.id } });
});
},
async store(ctx: any) {
const task = await Task.create({
user_id: ctx.user.id,
title: ctx.body.title,
completed: false,
});
// Enqueue email job
await Queue.dispatch('send_email', {
to: ctx.user.email,
subject: 'Task Created',
body: `You created task: ${task.title}`,
});
return task;
},
async update(ctx: any) {
const task = await Task.find(ctx.params.id);
if (!task) return ctx.status(404);
task.fill(ctx.body);
await task.save();
if (task.completed) {
// Send notification
await Notification.send('slack', {
message: `Task completed: ${task.title}`,
});
// Call external service (Ecommerce) to reward user
try {
await HttpClient.post('http://ecommerce-orders:3002/rewards', {
user_id: ctx.user.id,
reason: 'task_completion',
});
} catch (err) {
Logger.error('Failed to reward user', { err });
}
}
return task;
},
};✅ Expected:
- Tasks persist in Postgres.
indexcaches results.storeenqueues email.update(complete) sends notification + calls external service.
6) Cache (packages/cache-redis, packages/cache-mongodb)
Local default: memory cache (no infra).
Optional: Redis cache (requires REDIS_URL and redis container).
Adapter install pattern (in a consumer app):
import '@zintrust/cache-redis/register';✅ Expected:
- Listing tasks uses cache (hit on second request).
7) Queue (packages/queue-redis, queue-rabbitmq, queue-sqs)
Local default: in-memory queue.
Optional: Redis queue:
import '@zintrust/queue-redis/register';Provider-required (optional):
- RabbitMQ queue:
@zintrust/queue-rabbitmq(needs RabbitMQ running) - SQS queue:
@zintrust/queue-sqs(needs AWS credentials + queue URL)
✅ Expected:
- Creating a task enqueues a “send email” job.
- Worker dequeues and processes successfully.
8) Mail (packages/mail-*)
Local default: fake mailer (assert sends in tests).
import { MailFake } from '@zintrust/core/node';Provider-required (optional):
- SendGrid:
@zintrust/mail-sendgrid - Mailgun:
@zintrust/mail-mailgun - SMTP:
@zintrust/mail-smtp - Nodemailer:
@zintrust/mail-nodemailer
✅ Expected:
- After register, a welcome email is “sent”.
9) Storage (packages/storage-*)
Local default:
FakeStorage(tests)- local disk storage driver (dev)
import { FakeStorage } from '@zintrust/core/node';Optional provider steps:
- S3 (
@zintrust/storage-s3) with MinIO or AWS - R2 (
@zintrust/storage-r2) Cloudflare - GCS (
@zintrust/storage-gcs) Google
✅ Expected:
- Task can upload an attachment and later download it.
10) Notifications & Broadcast
Notifications (Slack/SMS)
import { sendSlackWebhook, sendSms } from '@zintrust/core';Broadcast (Real-time)
Broadcast events to connected clients (e.g., via WebSocket or SSE).
import { broadcast } from '@zintrust/core';
// In TasksController.update:
if (task.completed) {
await broadcast('task.completed', {
id: task.id,
title: task.title,
user_id: task.user_id,
});
}✅ Expected:
- On task completion, a notification is emitted.
- Connected clients receive the
task.completedevent.
11) Templates (Markdown templates)
import { MarkdownRenderer } from '@zintrust/core';Node-only template helpers (optional):
import {
listTemplates,
loadTemplate,
renderTemplate,
listNotificationTemplates,
loadNotificationTemplate,
renderNotificationTemplate,
} from '@zintrust/core/node';✅ Expected:
- Emails/notifications render Markdown templates.
12) Logging + HTTP client
import { HttpClient, Logger } from '@zintrust/core';✅ Expected:
- Calls to external services are logged.
- Sensitive fields are not logged.
13) Middleware: security + rate limiting + CSRF
This demo should include the built-in middleware suite (secure defaults, low friction).
import {
CsrfMiddleware,
ErrorHandlerMiddleware,
LoggingMiddleware,
RateLimiter,
SecurityMiddleware,
} from '@zintrust/core';
// Example: register globally during boot
app.getMiddlewareStack().register('log', LoggingMiddleware.create());
app.getMiddlewareStack().register('error', ErrorHandlerMiddleware.create());
app.getMiddlewareStack().register('security', SecurityMiddleware.create());
app.getMiddlewareStack().register('rateLimit', RateLimiter.create({ windowMs: 60_000, max: 100 }));
app.getMiddlewareStack().register('csrf', CsrfMiddleware.create());✅ Expected:
- Standard security headers are applied.
- Rate limiting returns
429when exceeded. - CSRF token cookie is issued on safe requests and validated on state-changing requests.
14) Adapter packages checklist (A–Z)
Version note (important, but simple):
- Adapter packages are meant to be version-aligned with
@zintrust/core. - In an app, install matching versions (for example
@zintrust/core@0.1.15with@zintrust/queue-redis@0.1.15). - In this monorepo, versions are synced from core during release.
This demo should touch each adapter package at least once:
- Cache:
@zintrust/cache-redis,@zintrust/cache-mongodb - DB:
@zintrust/db-sqlite,@zintrust/db-postgres,@zintrust/db-mysql,@zintrust/db-sqlserver,@zintrust/db-d1 - Mail:
@zintrust/mail-smtp,@zintrust/mail-nodemailer,@zintrust/mail-sendgrid,@zintrust/mail-mailgun - Queue:
@zintrust/queue-redis,@zintrust/queue-rabbitmq,@zintrust/queue-sqs - Storage:
@zintrust/storage-s3,@zintrust/storage-r2,@zintrust/storage-gcs - Cloudflare proxies:
@zintrust/cloudflare-d1-proxy,@zintrust/cloudflare-kv-proxy
15) Services checklist (A–Z)
Ecommerce services live under services/ecommerce/*.
Notes:
- The compose file exists under
services/ecommerce/docker-compose.yml. - Some docker build paths/Dockerfiles referenced by the compose may require syncing/generation.
Integration Example
When a task is completed, we call the Orders Service to issue a reward.
// src/services/Ecommerce.ts
import { HttpClient, Logger } from '@zintrust/core';
export const EcommerceService = {
async issueReward(userId: string) {
try {
// Call the service URL (e.g. via Docker network)
const response = await HttpClient.post('http://ecommerce-orders:3002/rewards', {
user_id: userId,
reason: 'task_completion',
amount: 10, // 10 points reward
});
return response.data;
} catch (err) {
Logger.error('Ecommerce service unavailable', { err });
// Fallback or retry logic
return null;
}
},
};✅ Expected:
- At minimum, Postgres in that compose is runnable.
- Once Dockerfile paths are aligned, users/orders/payments/gateway can be booted and called.
- The Tasks app successfully calls the Orders service (or logs an error if down).