@skyapp-labs/blueprint-backend-core
v1.5.0
Published
Pluggable NestJS core modules: auth, users, roles, notifications, profile.
Maintainers
Readme
@skyapp-labs/blueprint-backend-core
Pluggable NestJS library that provides a complete authentication, user management, RBAC, notifications, and infrastructure foundation for Blueprint projects. Import once and get production-ready endpoints, services, guards, and background jobs out of the box.
Table of Contents
- What's Included
- Requirements
- Installation
- Quick Start
- Environment Variables
- Database Setup
- Modules Reference
- Using Core Services
- Common Utilities
- App Settings Reference
- RBAC — Roles & Permissions
- Extending the Core
- Overriding a Core Module
- Development Scripts
- Versioning
What's Included
| Module | Endpoints | What it provides |
|--------|-----------|-----------------|
| Auth | /auth/* | Login, register, OTP, password reset, JWT, Keycloak, invite flow |
| Sessions | /auth/refresh, /auth/logout, /auth/sessions | Refresh token rotation, session list, revoke |
| Users | /users/* | User CRUD, search, pagination, deactivate, soft delete |
| Profile | /profile/* | Get/update profile, change phone (OTP-verified), delete account |
| Roles | /roles/*, /permissions/*, /modules/* | RBAC — roles, permissions, manifest-based auto-sync |
| Notifications | /notifications/* | In-app notifications, FCM push, device token management |
| Settings | /settings/* | DB-backed runtime configuration with in-memory cache |
| Admin | /admin/* | Dashboard stats, immutable audit log |
| Health | /health/* | Liveness, readiness, DB/Redis/memory checks |
| Jobs | — | BullMQ background queues for email, SMS, push |
| OTP | — | OTP engine — SMTP / SendGrid / Mailgun / Resend / Twilio / Termii / Infobip / SmartSMS |
Requirements
- Node.js >= 20
- PostgreSQL >= 14
- Redis (required for OTP, rate limiting, and background jobs)
Peer dependencies — these must be installed in your consuming app:
npm install \
@nestjs/bullmq@>=11 \
@nestjs/common@>=11 \
@nestjs/config@>=4 \
@nestjs/core@>=11 \
@nestjs/jwt@>=11 \
@nestjs/passport@>=11 \
@nestjs/swagger@>=11 \
@nestjs/terminus@>=11 \
@nestjs/throttler@>=6 \
@nestjs/typeorm@>=11 \
bullmq@>=5 \
class-transformer@>=0.5 \
class-validator@>=0.14 \
ioredis@>=5 \
passport@>=0.7 \
passport-jwt@>=4 \
reflect-metadata@>=0.2 \
rxjs@>=7 \
typeorm@>=0.3Installation
This package is published to the GitHub Package Registry, not the public npm registry. You must authenticate before you can install it.
Step 1 — Create a GitHub Personal Access Token
- Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Give it a note (e.g.
npm-packages-read) and select theread:packagesscope - Click Generate token and copy the value — you will only see it once
Step 2 — Configure your project's .npmrc
Create (or edit) an .npmrc file at the root of your project. Use an environment variable for the token so it is never hardcoded or committed:
@skyapp-labs:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Then set the token in your shell before running any npm command:
# Add to your shell profile (~/.zshrc or ~/.bashrc) for permanent access
export GITHUB_TOKEN=ghp_your_token_here
# Or set it inline for a one-off install
GITHUB_TOKEN=ghp_your_token_here npm installAdd
.npmrcto.gitignoreonly if you include a raw token in it. The version above using${GITHUB_TOKEN}is safe to commit — the value is read from the environment at install time.
Step 3 — Install the package
npm install @skyapp-labs/blueprint-backend-coreCI / CD (GitHub Actions)
The GITHUB_TOKEN secret is injected automatically in all GitHub Actions workflows — no extra secrets configuration needed. Just expose it as an environment variable:
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}With the .npmrc shown in Step 2 already committed to your repo, npm ci will authenticate correctly.
Quick Start
1. Configure your AppModule
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
CoreModule,
appConfig,
databaseConfig,
envValidation,
TypeOrmService,
} from '@skyapp-labs/blueprint-backend-core';
@Module({
imports: [
// 1. Load configuration — must come before CoreModule
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig, databaseConfig],
validate: envValidation,
}),
// 2. Wire up TypeORM using the built-in service (reads DATABASE_* env vars)
TypeOrmModule.forRootAsync({
useClass: TypeOrmService,
}),
// 3. Import CoreModule — all feature modules are registered automatically
CoreModule.forRoot(),
],
})
export class AppModule {}2. Selectively disable optional modules
CoreModule.forRoot({
modules: {
notifications: false, // disable FCM push notifications
admin: false, // disable admin dashboard & audit logs
health: false, // disable /health endpoints
},
})3. Apply global middleware in main.ts
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import {
HttpExceptionFilter,
LoggingInterceptor,
} from '@skyapp-labs/blueprint-backend-core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();Environment Variables
Copy .env.example from this package as a starting point.
Application
| Variable | Default | Required | Description |
|----------|---------|----------|-------------|
| NODE_ENV | development | | development | production | test |
| PORT | 3000 | | HTTP server port |
| APP_URL | — | | Full base URL (used in invite/reset emails) |
| APP_DEBUG | false | | Enable verbose logging |
| SWAGGER_ENABLED | auto | | Force-enable Swagger in production (true/false) |
| HTTP_REQUEST_TIMEOUT_MS | 30000 | | Global HTTP handler timeout (ms); clamped 1000–120000. Host registers TimeoutInterceptor with app.requestTimeoutMs. |
Database
| Variable | Default | Required | Description |
|----------|---------|----------|-------------|
| DATABASE_HOST | localhost | | PostgreSQL host |
| DATABASE_PORT | 5432 | | PostgreSQL port |
| DATABASE_USERNAME | — | yes | DB username |
| DATABASE_PASSWORD | — | yes | DB password |
| DATABASE_NAME | — | yes | DB name |
| DATABASE_SSL | false | | Enable SSL (true/false) |
| DATABASE_SSL_REJECT_UNAUTHORIZED | true* | | When SSL is on, verify server cert (false only if unavoidable) |
| DATABASE_SSL_CA | — | | Path to PEM CA bundle (e.g. RDS); optional if the server uses a public CA |
*When DATABASE_SSL=true, verification defaults to on unless you set DATABASE_SSL_REJECT_UNAUTHORIZED=false.
JWT
| Variable | Default | Required | Description |
|----------|---------|----------|-------------|
| JWT_SECRET | — | yes | Use a strong random string; there is no safe default in app.config — rely on envValidation |
| JWT_EXPIRATION | 1d | | Access token lifetime (e.g. 15m, 1h, 1d) |
Redis
| Variable | Default | Required | Description |
|----------|---------|----------|-------------|
| REDIS_ENABLED | false | | Set to true to enable OTP, rate limiting, and background jobs |
| REDIS_HOST | localhost | | Redis host |
| REDIS_PORT | 6379 | | Redis port |
| REDIS_PASSWORD | — | | Redis password (leave empty for local dev) |
| REDIS_TLS | false | | Enable TLS (for managed Redis — Upstash, ElastiCache, etc.) |
| REDIS_TLS_REJECT_UNAUTHORIZED | true* | | When TLS is on, verify server cert (false only if unavoidable) |
| REDIS_TLS_CA | — | | Path to PEM CA bundle; optional if the server uses a public CA |
*When REDIS_TLS=true, verification defaults to on unless you set REDIS_TLS_REJECT_UNAUTHORIZED=false.
When
REDIS_ENABLED=false, OTP flows and background jobs are gracefully disabled. JWT-based auth still works, but OTP sending/verification, per-IP rate limiting, and job queues are inactive.
Keycloak (optional)
Only required when using the Keycloak auth provider:
| Variable | Description |
|----------|-------------|
| KEYCLOAK_ISSUER_URL | e.g. https://auth.example.com/realms/myrealm |
| KEYCLOAK_REALM | Realm name |
| KEYCLOAK_CLIENT_ID | API client ID |
| KEYCLOAK_CLIENT_SECRET | Client secret (confidential clients only) |
| KEYCLOAK_ADMIN_URL | Admin API base URL |
| KEYCLOAK_ADMIN_USERNAME | Admin console username |
| KEYCLOAK_ADMIN_PASSWORD | Admin console password |
Seed (optional)
Used only by npm run seed to create the initial super admin:
| Variable | Description |
|----------|-------------|
| SUPER_ADMIN_EMAIL | Super admin email |
| SUPER_ADMIN_PHONE | Phone in E.164 format (e.g. +2348012345678) |
| SUPER_ADMIN_FIRST_NAME | First name |
| SUPER_ADMIN_LAST_NAME | Last name |
| SUPER_ADMIN_PASSWORD | Password (native auth only) |
| SUPER_ADMIN_COUNTRY_CODE | ISO 3166-1 alpha-2 (e.g. NG) |
Database Setup
TypeOrmService (exported from this package) automatically resolves entity and migration paths. Pass it to TypeOrmModule.forRootAsync as shown in the Quick Start and entities + migrations are wired up for you.
If you need to add your own entities or migrations alongside core ones, extend the service:
// typeorm.service.ts (in your app)
import { TypeOrmService as CoreTypeOrmService } from '@skyapp-labs/blueprint-backend-core';
import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { join } from 'path';
@Injectable()
export class TypeOrmService extends CoreTypeOrmService {
createTypeOrmOptions(): TypeOrmModuleOptions {
const base = super.createTypeOrmOptions() as Record<string, unknown>;
return {
...base,
entities: [
...(base.entities as string[]),
join(__dirname, '**/*.entity{.ts,.js}'),
],
migrations: [
...(base.migrations as string[]),
join(__dirname, 'database/migrations/*{.ts,.js}'),
],
};
}
}Run migrations
npm run migration:runRun seeds
Creates default roles, permissions, app settings, and optionally a super admin. Seeds are idempotent — safe to run multiple times.
npm run seedYou can also run seeds programmatically from your own project:
import { runCoreSeeds } from '@skyapp-labs/blueprint-backend-core';
await runCoreSeeds();Modules Reference
Auth
Handles the full authentication lifecycle with a pluggable provider system.
Auth providers (set via the auth.provider app setting):
| Provider | Description |
|----------|-------------|
| native | Email + password, or phone + OTP |
| keycloak | Delegates auth to a Keycloak instance |
Endpoints:
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /auth/config | Public | Returns active auth method (phone/email) and whether password reset is enabled |
| POST | /auth/register | Public | Self-registration |
| POST | /auth/login | Public | Login — email+password or phone+OTP |
| POST | /auth/send-otp | Public | Send OTP to phone or email |
| POST | /auth/verify-otp | Public | Verify OTP code, returns bridge token |
| POST | /auth/forgot-password | Public | Request password reset link |
| POST | /auth/reset-password | Public | Submit new password with reset token |
| POST | /auth/accept-invite | Public | Accept an email/phone invitation |
| DELETE | /auth/account | JWT | Delete own account |
Sessions
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | /auth/refresh | Public | Rotate refresh token, returns new token pair |
| POST | /auth/logout | JWT | Revoke all active sessions for the current user |
| GET | /auth/sessions | JWT | List all active sessions for the current user |
| POST | /auth/sessions/:sessionId/revoke | JWT | Revoke a specific session by ID |
Users
| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | /users | users:read | Paginated user list with search/filter |
| GET | /users/stats | users:read | User count breakdown (total/active/invited/suspended) |
| GET | /users/:id | users:read | Get user by ID |
| PATCH | /users/:id | users:update | Update user fields |
| DELETE | /users/:id | users:delete | Soft-delete user |
| POST | /users/:id/deactivate | users:deactivate | Suspend a user |
| POST | /users/:id/activate | users:activate | Reactivate a suspended user |
| POST | /users/invite | users:invite | Send an email/phone invite |
| POST | /users/:id/resend-invite | users:invite | Resend an invite |
| DELETE | /users/:id/invite | users:invite | Cancel a pending invite |
Profile
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /profile | JWT | Get own profile |
| PATCH | /profile | JWT | Update profile fields |
| POST | /profile/change-phone | JWT | Request phone number change (triggers OTP) |
| POST | /profile/change-phone/verify | JWT | Confirm phone change with OTP code |
| DELETE | /profile | JWT | Delete own account |
Roles & Permissions
Manifest-based RBAC. Modules declare their permissions in manifest files; the core syncs them to the database on startup automatically.
| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | /roles | roles:read | List roles |
| POST | /roles | roles:create | Create a role |
| PATCH | /roles/:id | roles:update | Update role / assign permissions |
| DELETE | /roles/:id | roles:delete | Delete a role |
| POST | /roles/users/:userId/roles | roles:assign | Assign a role to a user |
| DELETE | /roles/users/:userId/roles/:roleId | roles:assign | Remove a role from a user |
| GET | /modules | roles:read | List all registered modules |
| GET | /permissions | roles:read | List all permissions |
Defining permissions for your own module:
// billing/billing.manifest.ts
import type { ModuleManifest } from '@skyapp-labs/blueprint-backend-core';
export const BILLING_PERMISSIONS = {
READ: 'billing:read',
CHARGE: 'billing:charge',
} as const;
export const manifest: ModuleManifest = {
slug: 'billing',
name: 'Billing',
permissions: [
{ slug: BILLING_PERMISSIONS.READ, name: 'View billing' },
{ slug: BILLING_PERMISSIONS.CHARGE, name: 'Charge customer' },
],
};The manifest is auto-discovered at startup — no registration needed. Permissions appear in the database and can be assigned to roles via the API.
Protecting routes:
import { RequirePermission } from '@skyapp-labs/blueprint-backend-core';
import { BILLING_PERMISSIONS } from './billing.manifest';
@Get('invoices')
@RequirePermission(BILLING_PERMISSIONS.READ)
findAll() { ... }Notifications
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | /notifications/devices | JWT | Register an FCM device token |
| DELETE | /notifications/devices | JWT | Remove a device token |
| GET | /notifications/devices | JWT | List registered devices for the current user |
| GET | /notifications | JWT | Paginated in-app notification list |
| PATCH | /notifications/:id/read | JWT | Mark a notification as read |
| PATCH | /notifications/read-all | JWT | Mark all notifications as read |
Settings
DB-backed runtime configuration. All OTP templates, rate limits, token TTLs, and provider credentials are stored here and editable via the API — no app restart required.
| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | /settings | Authenticated | List all settings |
| PATCH | /settings/:key | Admin | Update a setting value |
Reading a setting in your service:
import { SettingsService, SETTING_KEYS } from '@skyapp-labs/blueprint-backend-core';
@Injectable()
export class YourService {
constructor(private readonly settings: SettingsService) {}
doSomething() {
const ttl = this.settings.get<number>(SETTING_KEYS.TOKENS_REFRESH_TTL_DAYS);
const provider = this.settings.get<string>(SETTING_KEYS.SMS_ACTIVE_PROVIDER);
}
}SettingsModule is @Global — inject SettingsService anywhere without importing the module.
Health
| Endpoint | Description |
|----------|-------------|
| GET /health/liveness | Always returns 200 — use as Kubernetes liveness probe |
| GET /health/readiness | Checks PostgreSQL connectivity — use as readiness probe |
| GET /health/full | Checks DB + Redis + heap memory + RSS memory + disk + BullMQ queue depth |
Admin
| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | /admin/dashboard | users:read | System stats: user counts, active sessions, uptime |
| GET | /admin/logs | users:read | Paginated admin audit log with filters (actor, action, date range) |
Jobs (BullMQ)
Three queues for fire-and-forget delivery. Use JobsQueue to enqueue jobs from any service:
import { JobsQueue } from '@skyapp-labs/blueprint-backend-core';
@Injectable()
export class YourService {
constructor(
@Optional() @Inject(JobsQueue) private readonly jobs: JobsQueue | null,
) {}
async notify() {
// Email
await this.jobs?.addSendEmail({
to: '[email protected]',
subject: 'Welcome',
template: 'welcome',
data: { name: 'Alice' },
});
// SMS
await this.jobs?.addSendSms({
to: '+2348012345678',
correlationId: 'some-id',
});
// Push notification
await this.jobs?.addSendPushNotification({
token: 'fcm-device-token',
title: 'New message',
body: 'You have a new message',
});
}
}Use @Optional() so your service degrades gracefully when REDIS_ENABLED=false.
Using Core Services
Import the module that owns a service, then inject the service normally in your provider.
// your-feature.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '@skyapp-labs/blueprint-backend-core';
import { YourFeatureService } from './your-feature.service';
@Module({
imports: [UsersModule],
providers: [YourFeatureService],
})
export class YourFeatureModule {}// your-feature.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '@skyapp-labs/blueprint-backend-core';
@Injectable()
export class YourFeatureService {
constructor(private readonly usersService: UsersService) {}
async getUserName(userId: string) {
const user = await this.usersService.findByIdOrFail(userId);
return user.fullName;
}
}Available services and their owning modules:
| Service | Import module | Purpose |
|---------|--------------|---------|
| UsersService | UsersModule | User CRUD — find, create, update, delete, ban, activate |
| InviteService | UsersModule | Generate and redeem invite tokens |
| AdminLogService | UsersModule | Write immutable audit log entries |
| SettingsService | SettingsModule (global) | Read/write DB-backed runtime config |
| RolesService | RolesModule | Role management |
| PermissionsService | RolesModule | Permission management |
| UserRolesService | RolesModule | Assign and revoke roles on users |
| TokenService | SessionsModule | Issue, rotate, and revoke JWT tokens |
| ProfileService | ProfileModule | Profile read/update logic |
| JobsQueue | JobsModule.register() | Enqueue email/SMS/push notification jobs |
Common Utilities
Base entities
import {
BaseEntity, // id, createdAt, updatedAt, deletedAt, version
ImmutableEntity // id, createdAt only — for ledger/audit records
} from '@skyapp-labs/blueprint-backend-core';
@Entity('orders')
export class Order extends BaseEntity {
@Column() amount!: number;
}Guards
import {
JwtAuthGuard, // validates Bearer JWT token
PermissionsGuard, // enforces @RequirePermission() on routes
LoginIpRateLimitGuard, // rate-limits login attempts by IP
OtpIpRateLimitGuard, // rate-limits OTP send requests by IP
} from '@skyapp-labs/blueprint-backend-core';Decorators
import {
CurrentUser, // injects the authenticated User from the JWT payload
Public, // marks a route as publicly accessible (no JWT required)
RequirePermission, // guards a route behind a permission slug
IpAddress, // injects the client IP address as a method parameter
UserAgent, // injects the User-Agent header as a method parameter
} from '@skyapp-labs/blueprint-backend-core';
@Get('me')
@UseGuards(JwtAuthGuard)
getProfile(@CurrentUser() user: User) { ... }
@Get('ping')
@Public()
ping() { return 'pong'; }
@Delete(':id')
@RequirePermission('users:delete')
remove(@Param('id') id: string, @IpAddress() ip: string) { ... }Pagination DTO
import { PaginationDto } from '@skyapp-labs/blueprint-backend-core';
@Get()
findAll(@Query() pagination: PaginationDto) {
// pagination.page, pagination.limit
}Filters & interceptors
import {
HttpExceptionFilter, // standardises all error response shapes
LoggingInterceptor, // logs method, path, status code, and response time
} from '@skyapp-labs/blueprint-backend-core';
// Apply globally in main.ts
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new LoggingInterceptor(configService));Infrastructure modules
import {
RedisModule, // REDIS_ENABLED-aware ioredis module
REDIS_CLIENT, // injection token for the raw ioredis client
FirebaseModule, // Firebase Admin SDK (credentials from Settings)
KeycloakModule, // Keycloak JWKS + token verification
} from '@skyapp-labs/blueprint-backend-core';App Settings Reference
All settings are stored in the app_settings table and editable at runtime via PATCH /settings/:key. Use SETTING_KEYS constants to reference them in code.
OTP
| Key | Description |
|-----|-------------|
| otp.ttl_seconds | OTP session lifetime before it expires |
| otp.max_attempts | Wrong-code attempts before session lockout |
| otp.resend_cooldown_seconds | Minimum wait between resend requests |
| otp.rate_limit_max | Max OTP sends per identifier within the window |
| otp.rate_limit_window_seconds | Rolling window for per-identifier rate limit |
| otp.ip_rate_limit_max | Max OTP requests per IP within the window |
| otp.ip_rate_limit_window_seconds | Rolling window for per-IP rate limit |
| otp.sms_template | SMS body — use {code} as placeholder |
| otp.email_subject | Email subject line |
| otp.email_html_template | HTML email body — use {code} as placeholder |
| otp.email_text_template | Plain-text email fallback |
Tokens
| Key | Description |
|-----|-------------|
| tokens.refresh_ttl_days | Refresh token lifetime in days |
| tokens.temporary_ttl_seconds | Bridge token lifetime after OTP verification |
| tokens.access_expires_in | Access token expiry (e.g. 15m, 1h) |
| tokens.invite_ttl_days | Invite link lifetime in days |
| tokens.password_reset_ttl_seconds | Password reset token lifetime |
Auth
| Key | Description |
|-----|-------------|
| auth.provider | Active auth provider: native | keycloak |
| auth.method | Primary identity method: phone | email |
| auth.default_role_slug | Role auto-assigned to every self-registered user (empty = no role) |
Login rate limiting
| Key | Description |
|-----|-------------|
| login.ip_rate_limit_max | Max login attempts per IP within the window |
| login.ip_rate_limit_window_seconds | Rolling window for per-IP login limit |
| login.lockout_max_attempts | Failed attempts before account is temporarily locked |
| login.lockout_duration_seconds | How long the lockout lasts |
Health thresholds
| Key | Description |
|-----|-------------|
| health.max_heap_mb | Heap memory threshold (MB) — triggers health warning |
| health.max_rss_mb | RSS memory threshold (MB) — triggers health warning |
| health.queue_depth_threshold | BullMQ queue depth above which health check warns |
SMS providers
| Key | Description |
|-----|-------------|
| sms.active_provider | twilio | termii | infobip | smartsms |
| sms.twilio_from_number | Twilio sender number (E.164) |
| sms.twilio_account_sid | Twilio account SID |
| sms.twilio_auth_token | Twilio auth token |
| sms.termii_api_key | Termii API key |
| sms.termii_sender_id | Termii sender ID |
| sms.infobip_api_key | Infobip API key |
| sms.infobip_sender_id | Infobip sender ID |
| sms.infobip_base_url | Infobip base URL (e.g. https://XXXXX.api.infobip.com) |
| sms.smartsms_token | SmartSMS auth token |
| sms.smartsms_sender_id | SmartSMS sender ID |
Email providers
| Key | Description |
|-----|-------------|
| email.active_provider | resend | sendgrid | mailgun | smtp |
| email.from_address | From address for all outbound emails |
| email.resend_api_key | Resend API key |
| email.sendgrid_api_key | SendGrid API key |
| email.mailgun_api_key | Mailgun API key |
| email.mailgun_domain | Mailgun sending domain |
| email.smtp_host | SMTP host |
| email.smtp_port | SMTP port (587 = STARTTLS, 465 = SSL) |
| email.smtp_user | SMTP username |
| email.smtp_pass | SMTP password |
Firebase (push notifications)
| Key | Description |
|-----|-------------|
| firebase.project_id | Firebase project ID |
| firebase.client_email | Service account client email |
| firebase.private_key | Service account private key (PEM) |
| firebase.api_key | Firebase web API key |
Testing
| Key | Description |
|-----|-------------|
| test.otp_identifiers | JSON array of test identifiers with fixed OTP codes. Schema: [{ "identifier": string, "channel": "sms"\|"email", "code": string }]. Hard-disabled in production regardless of this value. |
RBAC — Roles & Permissions
The RBAC system is manifest-driven. Here is the full flow from declaration to enforcement:
- Define a manifest in each feature module (see Roles & Permissions above)
- On startup, the core scans all manifests and upserts modules + permissions into the
modulesandpermissionstables — new records are inserted, existing ones are updated, nothing is ever deleted - Assign permissions to roles via the
/rolesAPI or a seed script - Assign roles to users via
POST /roles/users/:userId/roles - Protect routes with
@RequirePermission('module:action')
The JWT access token includes the user's permission slugs as a claim — there is no database query per request to check permissions.
Extending the Core
Add a linked entity to User
Never modify the User entity directly. Create a linked entity in your own module:
// modules/extended-profile/extended-profile.entity.ts
import { Entity, Column, OneToOne, JoinColumn } from 'typeorm';
import { BaseEntity, User } from '@skyapp-labs/blueprint-backend-core';
@Entity('extended_profiles')
export class ExtendedProfile extends BaseEntity {
@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@Column({ nullable: true })
bio?: string;
@Column({ nullable: true })
avatarUrl?: string;
}Write audit log entries
import { AdminLogService } from '@skyapp-labs/blueprint-backend-core';
this.adminLog.log({
actorId: adminUser.id,
action: 'billing.refund',
targetType: 'order',
targetId: orderId,
metadata: { amount: 5000 },
ipAddress: ip,
});AdminLog entries are stored in an ImmutableEntity table — they have no updatedAt or deletedAt and cannot be soft-deleted.
Overriding a Core Module
If a core module's default behavior does not fit your project, override it rather than forking or editing the package.
Step 1 — Disable the core module:
CoreModule.forRoot({
modules: { notifications: false },
})Or for modules without a disable flag, simply do not register them individually and instead import your replacement.
Step 2 — Create a replacement in src/modules/:
// src/modules/notifications/notifications.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '@skyapp-labs/blueprint-backend-core';
import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service';
@Module({
imports: [UsersModule],
controllers: [NotificationsController],
providers: [NotificationsService],
})
export class NotificationsModule {}Step 3 — Add it to your AppModule.
Your controller is now the only handler — the core version is not registered at runtime.
When upgrading the package, review the changelog for API changes in any core service your override depends on (e.g.
UsersService,SettingsService) and update accordingly.
Development Scripts
Run these from within this repository when developing the package itself.
# Install dependencies
npm install
# Build
npm run build
# Run unit tests
npm test
# Run tests with coverage report
npm run test:cov
# Run tests in CI mode (parallel, with coverage)
npm run test:ci
# Type-check without emitting files
npm run typecheck
# Lint
npm run lint
npm run lint:fix
# Format
npm run format
npm run format:check
# ── Database (requires a valid .env with DB credentials) ──────────────────
# Apply all pending migrations
npm run migration:run
# Generate a migration from entity changes (diff against current DB schema)
npm run migration:update
# Revert the last migration
npm run migration:revert
# Drop all tables
npm run schema:drop
# Drop and regenerate a full schema migration from scratch
npm run schema:create
# Seed the database (roles, permissions, settings, optional super admin)
npm run seedVersioning
This package uses semantic-release with Conventional Commits. Releases are published automatically when commits are merged to main.
| Commit prefix | Release type | Example |
|---------------|-------------|---------|
| fix: | Patch 1.0.x | fix: handle null phone on login |
| feat: | Minor 1.x.0 | feat: add bulk user deactivation |
| feat!: or BREAKING CHANGE: footer | Major x.0.0 | feat!: remove deprecated login endpoint |
The changelog is updated automatically in CHANGELOG.md on each release.
Install a specific version:
npm install @skyapp-labs/[email protected]Always install the latest:
npm install @skyapp-labs/blueprint-backend-core@latest