npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@skyapp-labs/blueprint-backend-core

v1.5.0

Published

Pluggable NestJS core modules: auth, users, roles, notifications, profile.

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

| 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.3

Installation

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

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Click Generate new token (classic)
  3. Give it a note (e.g. npm-packages-read) and select the read:packages scope
  4. 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 install

Add .npmrc to .gitignore only 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-core

CI / 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:run

Run seeds

Creates default roles, permissions, app settings, and optionally a super admin. Seeds are idempotent — safe to run multiple times.

npm run seed

You 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:

  1. Define a manifest in each feature module (see Roles & Permissions above)
  2. On startup, the core scans all manifests and upserts modules + permissions into the modules and permissions tables — new records are inserted, existing ones are updated, nothing is ever deleted
  3. Assign permissions to roles via the /roles API or a seed script
  4. Assign roles to users via POST /roles/users/:userId/roles
  5. 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 seed

Versioning

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