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

@nage-api/config

v1.0.0-beta.4

Published

One typed configuration for a @nage-api application — defineConfig, zod env validation, secret providers

Readme

@nage-api/config

One typed configuration for a @nage-api application, validated once at boot (PLAN.md §11).

Replaces the legacy pair of an env.ts singleton and @nestjs/config — two systems, no validation, env.get<T>() as T — with a single source: a committed defineConfig() literal, a zod schema for the environment, and a SecretProviderPort for everything that must not be committed.

Usage

// apps/api/src/config/env.schema.ts
import { baseEnvSchema, secretSchema } from '@nage-api/config';
import { z } from 'zod';

export const EnvSchema = baseEnvSchema.extend({
  DATABASE_URL: z.url(),
  JWT_PRIVATE_KEY: secretSchema,
});

export type Env = z.infer<typeof EnvSchema>;
// apps/api/src/config/nage.config.ts
import { defineConfig, defineConfigFragment } from '@nage-api/config';

import type { Env } from './env.schema.js';

// Normally imported from `@app/config`, shared by every app in the workspace.
const workspaceDefaults = defineConfigFragment({
  logging: { json: true },
  database: { driver: 'postgres', ssl: 'verify-full' },
});

export const buildConfig = (env: Env) =>
  defineConfig(
    {
      app: { name: 'my-api', environment: env.NODE_ENV, port: env.PORT },
      // Omitting `cors` disables it; an allow-list is what switches it on.
      http: {
        ...(env.CORS_ORIGINS === undefined ? {} : { cors: { origins: env.CORS_ORIGINS } }),
      },
      database: { enabled: true, driver: 'postgres', url: env.DATABASE_URL },
      secrets: { provider: 'aws', prefix: 'prod/my-api/' },
    },
    { extends: [workspaceDefaults] },
  );
// apps/api/src/app.module.ts
import { NageConfigModule } from '@nage-api/config';
import { NageCoreModule } from '@nage-api/core';
import { Module } from '@nestjs/common';

import { EnvSchema } from './config/env.schema.js';
import { buildConfig } from './config/nage.config.js';

const config = buildConfig(EnvSchema.parse(process.env));

@Module({
  imports: [
    // Core first: both modules publish NAGE_CONFIG, and the later import wins.
    NageCoreModule.forRoot(config),
    NageConfigModule.forRoot({ config, envSchema: EnvSchema }),
  ],
})
export class AppModule {}
// apps/api/src/main.ts
import { loadEnvOrExit } from '@nage-api/config';
import { bootstrap } from '@nage-api/core';

import { AppModule } from './app.module.js';
import { EnvSchema } from './config/env.schema.js';
import { buildConfig } from './config/nage.config.js';

const env = loadEnvOrExit(EnvSchema); // invalid env → stderr report, exit 1
void bootstrap({ module: AppModule, config: buildConfig(env) });
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nage-api/config';

import type { Env } from './env.schema.js';

@Injectable()
export class ReportService {
  constructor(@Inject(ConfigService) private readonly config: ConfigService<Env>) {}

  async run(): Promise<void> {
    this.config.env('DATABASE_URL'); // string, typed by the schema
    this.config.isEnabled('database'); // feature toggles (§11.1 item 6)
    await this.config.requireSecret('JWT_PRIVATE_KEY');
  }
}

The layers (§11.1)

| # | Layer | Where | | --- | ------------------ | ---------------------------------------------------------------- | | 1 | Framework defaults | withDefaults() — applied when the config is published | | 2 | Workspace defaults | defineConfigFragment() in @app/config, passed via extends | | 3 | App config | the app's own defineConfig() literal | | 4 | Environment | env.schema.ts, validated by loadEnv/loadEnvOrExit | | 5 | Secrets | SecretProviderPort — env, AWS Secrets Manager, or your own | | 6 | Feature flags | enabled per block; a disabled feature contributes no providers |

Later layers win. Arrays replace rather than concatenate: a CORS allow-list is a complete statement of policy, and appending to an inherited one is how an override silently widens it.

Behaviour worth knowing

Failures are complete and early. loadEnv reports every problem at once, so a misconfigured deployment is fixed in one pass instead of one restart per variable. NageConfigModule.forRoot() validates while the module is being built — before a port is bound.

Reports never publish what they were validating. For variables whose name contains SECRET, PASSWORD, TOKEN, KEY, CREDENTIAL, DSN, URL or URI, the message is reduced to "is required but not set" / "is set but has the wrong shape (value hidden)". Env reports land in CI logs, and expected url, received postgres://user:pw@… would publish the credential it was rejecting.

The env is only validated if you hand over the schema. forRoot({ config }) without envSchema publishes an empty environment, so config.env('X') is typed by your schema and undefined at run time. Passing envSchema is what makes ConfigService<Env> true rather than a claim.

Errors separate the two audiences. Error.message names the variable, the section or the secret for the operator and the log; the client-facing payload stays { code: 'CONFIGURATION_INVALID', message: 'Internal server error' }.

Secrets are a port. EnvSecretProvider is the default; AwsSecretProvider loads the SDK lazily and treats it as an optional peer, so an app using env secrets carries no AWS client. CachingSecretProvider wraps either with a TTL, because a signing key should not be a per-request network call.

Defaults are the safe ones. verify-full TLS, a bounded maxQueryLimit, RS256 with 15m access tokens, rotating refresh tokens with reuse detection, argon2id passwords — applied whenever the relevant block is present, and overridable in the open where nage doctor (Phase 4) can see it.

Where the types live

The config types (NageConfig and every block) are declared in @nage-api/contracts, not here. Feature packages may not import each other, so a package reading its own config block must find that type at the bottom of the graph. This package owns the runtime that produces and validates a value.

Not yet implemented

  • The vault secret provider. secrets.provider: 'vault' throws a ConfigurationError naming the two that work, rather than falling back to a provider you did not ask for.
  • Runtime configuration — the Settings module of §11.1 item 7, for values that change without a redeploy. Everything here is settled at boot.
  • Reading a .env file. Nothing in this package touches one: loadEnv reads process.env, and populating it is the job of the process manager, docker compose, or node --env-file=.env. nage doctor reads the workspace's .env for its own audit, which is a different thing.

The deeper guide is docs/packages/config.md.