npm package: @skyapp-labs/blueprint-backend-core
Description: Pluggable NestJS core modules: auth, users, roles, notifications, profile.
Author: Skyapp Labs
Latest version: 1.0.10

# @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](#whats-included)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Database Setup](#database-setup)
- [Modules Reference](#modules-reference)
- [Using Core Services](#using-core-services)
- [Common Utilities](#common-utilities)
- [App Settings Reference](#app-settings-reference)
- [RBAC — Roles & Permissions](#rbac--roles--permissions)
- [Extending the Core](#extending-the-core)
- [Overriding a Core Module](#overriding-a-core-module)
- [Development Scripts](#development-scripts)
- [Versioning](#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:

```bash
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)](https://github.com/settings/tokens)
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:

```bash
# 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

```bash
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:

```yaml
- 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`

```typescript
// 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

```typescript
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`

```typescript
// 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`) |

### 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`) |

### JWT

| Variable | Default | Required | Description |
|----------|---------|----------|-------------|
| `JWT_SECRET` | — | **yes** | Use a strong random string in production |
| `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.) |

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

```typescript
// 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

```bash
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.

```bash
npm run seed
```

You can also run seeds programmatically from your own project:

```typescript
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:**

```typescript
// 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:**

```typescript
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:**

```typescript
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:

```typescript
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: 'user@example.com',
      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.

```typescript
// 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 {}
```

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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

```typescript
import { PaginationDto } from '@skyapp-labs/blueprint-backend-core';

@Get()
findAll(@Query() pagination: PaginationDto) {
  // pagination.page, pagination.limit
}
```

### Filters & interceptors

```typescript
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

```typescript
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](#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:

```typescript
// 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

```typescript
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:**

```typescript
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/`:**

```typescript
// 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.

```bash
# 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](https://semantic-release.gitbook.io/) with [Conventional Commits](https://www.conventionalcommits.org/). 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:**

```bash
npm install @skyapp-labs/blueprint-backend-core@1.2.3
```

**Always install the latest:**

```bash
npm install @skyapp-labs/blueprint-backend-core@latest
```