IGNIS Framework Patterns
All backend services are built on the IGNIS Framework (@venizia/ignis + @venizia/ignis-helpers). This page documents the patterns every service follows.
Application Hierarchy
IssuerApplication— only@nx/identityextends this (it signs JWTs and serves the JWKS endpoint)VerifierApplication— all other services extend this (they verify JWTs by fetching identity's/jw-certs)
Application class pattern
Every service's src/application.ts follows this exact structure:
import { createAppConfig, VerifierApplication } from '@nx/core';
export const appConfig = createAppConfig();
export class Application extends VerifierApplication {
override async boot() { return {}; }
override getAppInfo() { return packageJson; }
override getProjectRoot() { /* bind and return __dirname */ }
override configureDatasources(): void {
super.configureDatasources();
this.dataSource(PostgresCoreDataSource);
}
override configureRepositories(): void {
super.configureRepositories();
this.repository(MyRepository);
}
override configureServices(): void {
super.configureServices();
this.service(MyService);
}
override configureControllers(): void {
super.configureControllers();
this.controller(MyController);
}
override configureComponents(): void {
super.configureComponents();
this.useCacheRedis({ bindingKey: BindingKeys.APPLICATION_REDIS_CACHE });
}
}Component Structure
Components group related DI registrations (repositories, services, controllers) that can be added as a unit:
export class MyComponent extends BaseComponent {
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
protected application: BaseApplication,
) {
super({
scope: MyComponent.name,
initDefault: { enable: true, container: application },
bindings: {},
});
}
override async binding(): Promise<void> {
this.application.repository(MyRepository);
this.application.service(MyService);
this.application.controller(MyController);
}
}Register in Application.configureComponents():
this.component(MyComponent);Dependency Injection
IGNIS uses decorator-based DI. Inject dependencies in service/controller constructors:
import { inject, BaseService, BindingKeys, BindingNamespaces } from '@venizia/ignis';
export class MyService extends BaseService {
constructor(
@inject({
key: BindingKeys.build({
namespace: BindingNamespaces.REPOSITORY,
key: MyRepository.name,
}),
})
private myRepository: MyRepository,
@inject({ key: 'custom/binding/key' })
private redis: DefaultRedisHelper,
) {
super({ scope: MyService.name });
}
}Binding key patterns
| Pattern | Example |
|---|---|
| Repositories | BindingKeys.build({ namespace: BindingNamespaces.REPOSITORY, key: 'MyRepository' }) |
| Services | BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: 'MyService' }) |
| Custom | String literal, e.g. '@nx/licensing/redis/cache' |
BaseService provides this.logger with this.logger.for(methodName) returning { info, error, warn, debug }.
Controller Factory (Auto CRUD)
ControllerFactory.defineCrudController() generates a full REST controller with standardized routes:
const _Controller = ControllerFactory.defineCrudController({
repository: { name: MyRepository.name },
authenticate: { strategies: [AuthenticateStrategy.JWT, AuthenticateStrategy.BASIC] },
authorize: { action: AuthorizationActions.READ, resource: MyPermissions.FIND.code },
controller: {
name: 'MyController',
basePath: '/my-entities',
isStrict: { path: true, requestSchema: true },
},
entity: () => MyEntity,
routes: {
find: { authorize: { action: AuthorizationActions.READ, resource: '...' } },
findById: { authorize: { ... } },
count: { authorize: { ... } },
findOne: { authorize: { ... } },
create: { request: { body: MyInsertSchema }, authorize: { ... } },
updateById: { request: { body: MyUpdateSchema }, authorize: { ... } },
deleteById: { authorize: { ... } },
deleteBy: { authorize: { ... } },
},
});
@controller({ path: '/my-entities', transport: ControllerTransports.REST })
export class MyController extends _Controller {
constructor(
@inject({ key: 'repositories.MyRepository' })
protected readonly repository: MyRepository,
) {
super(repository);
}
}This generates 10 CRUD routes: find, findById, findOne, count, create, updateById, updateBy, deleteById, deleteBy. Custom routes are added via @get(), @post(), etc. decorators on additional methods.
Permission codes
crudPermissions(subject, labels) generates permission objects with codes:
| Code | Action |
|---|---|
Subject.find | List |
Subject.findById | Get by ID |
Subject.findOne | Find one |
Subject.count | Count |
Subject.create | Create |
Subject.updateById | Update by ID |
Subject.updateBy | Batch update |
Subject.deleteById | Delete by ID |
Subject.deleteBy | Batch delete |
Custom action permissions are defined manually (see licensing's License.issue, License.suspend, etc.).
Bootstrap Helpers
Application entry point
// packages/*/src/index.ts
import { bootstrapApplication } from '@nx/core';
import { Application, appConfig } from './application';
bootstrapApplication({
ApplicationClass: Application,
config: appConfig,
options: { bannerPath: resolve(__dirname, '../resources/banner.txt') },
});Migration entry point
// packages/*/src/migrate.ts
import { bootstrapMigration } from '@nx/core';
import { Application } from './application';
import { getMigrationProcesses } from './migrations/processes/migration-process';
bootstrapMigration({ ApplicationClass: Application, getMigrationProcesses });Migration process loader
// packages/*/src/migrations/processes/migration-process.ts
import { createMigrationProcessLoader } from '@nx/core';
export const getMigrationProcesses = createMigrationProcessLoader({
seedPaths: ['my-package-0001-seed-permissions'],
importFn: path => import(`../processes/${path}.js`),
});Each seed file exports a TMigrationProcess with options: { alwaysRun?: boolean }, a name, and a migrateFn.
Database Patterns
Drizzle ORM with PostgreSQL
Every service shares a single PostgresCoreDataSource:
@datasource({ driver: 'node-postgres' })
export class PostgresCoreDataSource extends BaseDataSource {
override configure(): void {
const schema = this.getSchema();
this.pool = new Pool(this.settings);
this.connector = drizzle({ client: this.pool, schema });
}
}Soft-deletable repository
SoftDeletableRepository from @nx/core automatically filters out rows with deleted_at IS NOT NULL:
@repository({ dataSource: PostgresCoreDataSource, model: MyEntity })
export class MyRepository extends SoftDeletableRepository<TSchema, TEntity, TPersist> {
// Inherited: deleteById sets deleted_at instead of hard-deleting
// findById, find, findOne all exclude soft-deleted rows
}Transactions
const tx = await this.repository.beginTransaction();
try {
await this.repository.create({ data, options: { transaction: tx } });
await this.otherRepository.updateById({ id, data, options: { transaction: tx } });
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}Row-level locking for concurrency safety:
const row = await this.repository.findOne({
filter: { where: { id } },
options: { transaction: tx, lock: { strength: LockStrengths.UPDATE } },
});Snowflake IDs
All primary keys are Snowflake IDs (64-bit integers as strings) generated by IdGenerator.getInstance().nextId(). Each service has a unique APP_ENV_SNOWFLAKE_WORKER_ID to prevent collisions.
Model Pattern
Drizzle schema + Zod validation:
// Schema definition (in @nx/core)
export const MyEntitySchema = licensingSchema.table('MyEntity', {
...generateCommonColumnDefs(), // id, createdAt, modifiedAt, deletedAt
name: text('name').notNull(),
status: text('status').notNull().default('activated'),
data: jsonb('data').$type<MyJsonType>(),
}, def => [
index('IDX_MyEntity_status').on(def.status),
]);
// Type exports
export type TMyEntity = TTableObject<typeof MyEntitySchema>;
export type TMyEntityPersist = TTableInsert<typeof MyEntitySchema>;
// Zod schemas (auto-generated from Drizzle)
export const MyEntitySelectSchema = createSelectSchema(MyEntitySchema);
export const MyEntityInsertSchema = createInsertSchema(MyEntitySchema);
export const MyEntityUpdateSchema = MyEntityInsertSchema.partial();generateCommonColumnDefs() adds: id (Snowflake, text PK), createdAt, modifiedAt, deletedAt (soft-delete).
For entities without soft-delete:
{
...generateIdColumnDefs({ id: { dataType: 'string' } }),
...generateTzColumnDefs({ deleted: { enable: false } }),
}Entity Relations
@model({
type: 'entity',
settings: { hiddenProperties: ['createdAt', 'modifiedAt', 'deletedAt'] },
})
export class MyEntity extends BaseEntity<typeof MyEntitySchema> {
static override TABLE_NAME = 'MyEntity';
static override schema = MyEntitySchema;
static override relations = (): TRelationConfig[] => [
{ type: RelationTypes.ONE, name: 'parent', schema: ParentSchema, ... },
{ type: RelationTypes.MANY, name: 'children', schema: ChildSchema, ... },
];
}Relations are used by the CRUD controller for include queries — e.g., ?filter[include][0][relation]=children.
Environment Configuration
Each package uses dotenv-flow for environment management:
| File | Purpose | Gitignored |
|---|---|---|
.env.development | Dev server config | No |
.env.test | Test config | Usually |
.env.local | Local overrides | Yes |
The NODE_ENV value determines which file is loaded. See Environment Reference for the complete variable catalog.
JWKS key generation (ES256, PKCS#8)
For the identity service only:
# Generate keypair
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
| tee private.pem | openssl pkey -pubout > public.pem
# Format for .env (single-line with \n)
awk 'NF {printf "%s\\n", $0}' private.pem
awk 'NF {printf "%s\\n", $0}' public.pemPrivate key must be PKCS#8 format (-----BEGIN PRIVATE KEY-----), not SEC1.
Key Source Files
| Pattern | File path |
|---|---|
| Application hierarchy | packages/core/src/application/{base,default,issuer,verifier}.ts |
| Soft-delete repository | packages/core/src/repositories/soft-deletable.repository.ts |
| Snowflake ID generator | packages/core/src/utilities/id-generator.utility.ts |
| Constants & roles | packages/core/src/common/constants.ts |
| Environment keys | packages/core/src/common/environments.ts |
| App config factory | packages/core/src/common/app-config.ts |
| Bootstrap helpers | packages/core/src/helpers/bootstraps/application.ts |
| Migration helpers | packages/core/src/helpers/migration/ |
| CRUD permissions | packages/core/src/common/constants.ts → crudPermissions() |
| Service applications | packages/*/src/application.ts |
Related Pages
| Page | Description |
|---|---|
| Getting Started | Local setup walkthrough |
| Build System | Makefile targets and dependency graph |
| Environment Reference | Complete env var catalog |
| Core Package | @nx/core detailed reference |