Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 74 additions & 28 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,34 +114,34 @@ These values **must** be provided via environment variables:

These values have defaults and can be overridden:

| Variable | Type | Default | Description |
| -------------------------------------- | ------- | ------------- | ----------------------------- |
| `PORT` | number | `3000` | Server port |
| `MODE` | enum | `development` | Environment mode |
| `DB_QUERY_TIMEOUT_MS` | number | `5000` | Prisma query timeout in ms |
| `APP_SECRET` | string | (dev key) | Secret for signing/encryption |
| `API_VERSION` | string | `1.0.0` | API version string |
| `ENABLE_RESPONSE_TIMING` | boolean | `true` | Enable timing headers |
| `ENABLE_API_VERSION_HEADER` | boolean | `true` | Enable version header |
| `ENABLE_SCHEMA_VERSION_HEADER` | boolean | `true` | Enable schema header |
| `ENABLE_REQUEST_LOGGING` | boolean | `true` | Enable request logging |
| `INDEXER_JITTER_FACTOR` | number | `0.1` | Jitter factor (0-1) |
| `BACKGROUND_JOB_LOCK_TTL_MS` | number | `300000` | Job lock TTL (5 min) |
| `CREATOR_LIST_SLOW_QUERY_THRESHOLD_MS` | number | `500` | Slow query threshold |
| `INDEXER_CURSOR_STALE_AGE_WARNING_MS` | number | `300000` | Stale cursor warning (5 min) |
| `INDEXER_HEARTBEAT_STALE_THRESHOLD_MS` | number | `300000` | Heartbeat stale threshold |
| `ENABLE_INDEXER_DEDUPE` | boolean | `true` | Enable dedupe guard |
| `ENABLE_INDEXER_DLQ` | boolean | `true` | Enable indexer dead-lettering |
| `ENABLE_INDEXER_CURSOR_STALENESS_WARNING` | boolean | `true` | Warn on stale cursors |
| `STELLAR_NETWORK` | enum | `testnet` | Stellar network selection |
| `STELLAR_HORIZON_URL` | URL | testnet URL | Horizon endpoint |
| `STELLAR_SOROBAN_RPC_URL` | URL | testnet URL | Soroban RPC endpoint |
| `OWNERSHIP_SNAPSHOT_TABLE_NAME` | string | `creator_ownership_snapshots` | Snapshot table name |
| `OWNERSHIP_SNAPSHOT_CLEANUP_DRY_RUN` | boolean | `true` | Log deletes without executing |
| `OWNERSHIP_SNAPSHOT_RETENTION_DAYS` | number | `30` | Retention window in days |
| `OWNERSHIP_SNAPSHOT_CLEANUP_ENABLED` | boolean | `false` | Enable cleanup scheduler |
| `OWNERSHIP_SNAPSHOT_CLEANUP_INTERVAL_MINUTES` | number | `60` | Cleanup scheduler interval |
| `PAYSTACK_PUBLIC_KEY` | string | (optional) | Paystack public key |
| Variable | Type | Default | Description |
| --------------------------------------------- | ------- | ----------------------------- | ----------------------------- |
| `PORT` | number | `3000` | Server port |
| `MODE` | enum | `development` | Environment mode |
| `DB_QUERY_TIMEOUT_MS` | number | `5000` | Prisma query timeout in ms |
| `APP_SECRET` | string | (dev key) | Secret for signing/encryption |
| `API_VERSION` | string | `1.0.0` | API version string |
| `ENABLE_RESPONSE_TIMING` | boolean | `true` | Enable timing headers |
| `ENABLE_API_VERSION_HEADER` | boolean | `true` | Enable version header |
| `ENABLE_SCHEMA_VERSION_HEADER` | boolean | `true` | Enable schema header |
| `ENABLE_REQUEST_LOGGING` | boolean | `true` | Enable request logging |
| `INDEXER_JITTER_FACTOR` | number | `0.1` | Jitter factor (0-1) |
| `BACKGROUND_JOB_LOCK_TTL_MS` | number | `300000` | Job lock TTL (5 min) |
| `CREATOR_LIST_SLOW_QUERY_THRESHOLD_MS` | number | `500` | Slow query threshold |
| `INDEXER_CURSOR_STALE_AGE_WARNING_MS` | number | `300000` | Stale cursor warning (5 min) |
| `INDEXER_HEARTBEAT_STALE_THRESHOLD_MS` | number | `300000` | Heartbeat stale threshold |
| `ENABLE_INDEXER_DEDUPE` | boolean | `true` | Enable dedupe guard |
| `ENABLE_INDEXER_DLQ` | boolean | `true` | Enable indexer dead-lettering |
| `ENABLE_INDEXER_CURSOR_STALENESS_WARNING` | boolean | `true` | Warn on stale cursors |
| `STELLAR_NETWORK` | enum | `testnet` | Stellar network selection |
| `STELLAR_HORIZON_URL` | URL | testnet URL | Horizon endpoint |
| `STELLAR_SOROBAN_RPC_URL` | URL | testnet URL | Soroban RPC endpoint |
| `OWNERSHIP_SNAPSHOT_TABLE_NAME` | string | `creator_ownership_snapshots` | Snapshot table name |
| `OWNERSHIP_SNAPSHOT_CLEANUP_DRY_RUN` | boolean | `true` | Log deletes without executing |
| `OWNERSHIP_SNAPSHOT_RETENTION_DAYS` | number | `30` | Retention window in days |
| `OWNERSHIP_SNAPSHOT_CLEANUP_ENABLED` | boolean | `false` | Enable cleanup scheduler |
| `OWNERSHIP_SNAPSHOT_CLEANUP_INTERVAL_MINUTES` | number | `60` | Cleanup scheduler interval |
| `PAYSTACK_PUBLIC_KEY` | string | (optional) | Paystack public key |

**Startup Behavior:** Uses default if not provided in environment.

Expand All @@ -161,6 +161,52 @@ export const appConfig = {

**Source:** Computed at startup from `envConfig` values.

## Startup Configuration Summary

On boot, the server emits a single structured log line summarizing the loaded
runtime configuration. This lets operators confirm how the process is
configured without inspecting the environment directly.

The summary is a **curated subset** of the configuration — the environment
context and the key feature flags — not a full environment dump. It is built by
`buildStartupConfigSummary` in [`src/utils/config-summary.utils.ts`](../src/utils/config-summary.utils.ts)
and logged by [`src/server.ts`](../src/server.ts):

```jsonc
{
"level": 30,
"msg": "Loaded runtime configuration summary",
"environment": {
"mode": "development",
"port": 3000,
"apiVersion": "1.0.0",
"stellarNetwork": "testnet",
"backendUrl": "http://localhost:3000",
"frontendUrl": "http://localhost:5173",
},
"featureFlags": {
"responseTiming": true,
"apiVersionHeader": true,
"schemaVersionHeader": true,
"requestLogging": true,
"indexerDedupe": true,
"indexerDlq": true,
"indexerCursorStalenessWarning": true,
"ownershipSnapshotCleanup": false,
},
}
```

**No secrets are logged.** Values flow through `maskSensitiveConfigValues`
([`src/utils/config-mask.utils.ts`](../src/utils/config-mask.utils.ts)), and
only non-sensitive keys are selected. Secrets, passwords, API keys, tokens, and
the `DATABASE_URL` credentials are never included.

**Validate locally:** start the server (`pnpm dev`) and look for the
`Loaded runtime configuration summary` line. To add a field to the summary,
extend `buildStartupConfigSummary` and its test
(`src/utils/config-summary.utils.test.ts`).

## Configuration by Environment

### Development
Expand Down
13 changes: 7 additions & 6 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { checkOptionalDependencies } from './utils/startup.utils';
import { describeDatabasePoolConfig } from './utils/db-pool-config.utils';
import { stopOwnershipSnapshotCleanupJob } from './jobs/ownership-snapshot-cleanup.job';
import { maskSensitiveConfigValues } from './utils/config-mask.utils';
import { buildStartupConfigSummary } from './utils/config-summary.utils';

async function startServer() {
try {
Expand Down Expand Up @@ -42,12 +42,13 @@ async function startServer() {
'Database connection pool configured'
);

// Log startup config summary with sensitive values masked.
// See `maskSensitiveConfigValues` in utils/config-mask.utils.ts for
// the list of patterns considered sensitive.
// Emit a structured summary of the loaded runtime config: environment
// context and key feature flags. Values flow through the masking helper,
// so no secrets or credentials are logged. See
// utils/config-summary.utils.ts for the curated field selection.
logger.info(
maskSensitiveConfigValues(),
'Startup configuration summary'
buildStartupConfigSummary(),
'Loaded runtime configuration summary'
);

// Verify migrations on startup
Expand Down
92 changes: 92 additions & 0 deletions src/utils/config-summary.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
jest.mock('../config', () => ({
envConfig: {
PORT: 3000,
MODE: 'test',
DATABASE_URL: 'postgresql://user:supersecret@localhost:5432/testdb',
GMAIL_USER: 'test@example.com',
GMAIL_APP_PASSWORD: 'my-app-password',
GOOGLE_CLIENT_ID: 'test-client-id',
GOOGLE_CLIENT_SECRET: 'test-client-secret',
BACKEND_URL: 'http://localhost:3000',
FRONTEND_URL: 'http://localhost:5173',
CLOUDINARY_CLOUD_NAME: 'test-cloud',
CLOUDINARY_API_KEY: 'test-api-key',
CLOUDINARY_API_SECRET: 'test-api-secret',
PAYSTACK_SECRET_KEY: 'sk_test_123456789',
PAYSTACK_PUBLIC_KEY: 'pk_test_123456789',
APP_SECRET: 'accesslayer_test_secret_key_32_bytes_long_xxxx',
STELLAR_NETWORK: 'testnet',
STELLAR_HORIZON_URL: 'https://horizon-testnet.stellar.org',
STELLAR_SOROBAN_RPC_URL: 'https://soroban-testnet.stellar.org',
ENABLE_RESPONSE_TIMING: true,
API_VERSION: '1.0.0',
ENABLE_API_VERSION_HEADER: true,
ENABLE_SCHEMA_VERSION_HEADER: false,
ENABLE_REQUEST_LOGGING: true,
DB_QUERY_TIMEOUT_MS: 5000,
INDEXER_JITTER_FACTOR: 0.1,
BACKGROUND_JOB_LOCK_TTL_MS: 300000,
SLOW_QUERY_THRESHOLD_MS: 500,
CREATOR_LIST_SLOW_QUERY_THRESHOLD_MS: 500,
INDEXER_CURSOR_STALE_AGE_WARNING_MS: 300000,
INDEXER_HEARTBEAT_STALE_THRESHOLD_MS: 300000,
ENABLE_INDEXER_DEDUPE: true,
ENABLE_INDEXER_DLQ: false,
ENABLE_INDEXER_CURSOR_STALENESS_WARNING: true,
OWNERSHIP_SNAPSHOT_TABLE_NAME: 'creator_ownership_snapshots',
OWNERSHIP_SNAPSHOT_CLEANUP_DRY_RUN: true,
OWNERSHIP_SNAPSHOT_RETENTION_DAYS: 30,
OWNERSHIP_SNAPSHOT_CLEANUP_ENABLED: false,
OWNERSHIP_SNAPSHOT_CLEANUP_INTERVAL_MINUTES: 60,
},
}));

import { buildStartupConfigSummary } from './config-summary.utils';

describe('buildStartupConfigSummary', () => {
it('reports the environment context', () => {
const summary = buildStartupConfigSummary();
expect(summary.environment).toEqual({
mode: 'test',
port: 3000,
apiVersion: '1.0.0',
stellarNetwork: 'testnet',
backendUrl: 'http://localhost:3000',
frontendUrl: 'http://localhost:5173',
});
});

it('reports the key feature flags with their values', () => {
const summary = buildStartupConfigSummary();
expect(summary.featureFlags).toEqual({
responseTiming: true,
apiVersionHeader: true,
schemaVersionHeader: false,
requestLogging: true,
indexerDedupe: true,
indexerDlq: false,
indexerCursorStalenessWarning: true,
ownershipSnapshotCleanup: false,
});
});

it('does not include any sensitive config values', () => {
const summary = buildStartupConfigSummary();
const serialized = JSON.stringify(summary);

expect(serialized).not.toContain('supersecret');
expect(serialized).not.toContain('my-app-password');
expect(serialized).not.toContain('test-client-secret');
expect(serialized).not.toContain('test-api-secret');
expect(serialized).not.toContain('sk_test_123456789');
expect(serialized).not.toContain('accesslayer_test_secret_key');
});

it('exposes only the environment and featureFlags groups', () => {
const summary = buildStartupConfigSummary();
expect(Object.keys(summary).sort()).toEqual([
'environment',
'featureFlags',
]);
});
});
69 changes: 69 additions & 0 deletions src/utils/config-summary.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { maskSensitiveConfigValues } from './config-mask.utils';

/**
* Structured summary of the loaded runtime configuration, emitted once at
* startup. The summary is deliberately a curated subset of `envConfig` rather
* than a full dump: it surfaces the environment context and the key feature
* flags an operator needs to confirm how the process is configured, without
* the noise of every tuning knob.
*
* Values are sourced through {@link maskSensitiveConfigValues}, so even though
* only non-sensitive keys are selected here, any value that flows through is
* already redacted. No secrets, passwords, keys, tokens, or connection-string
* credentials are included.
*/
export interface StartupConfigSummary {
environment: {
mode: unknown;
port: unknown;
apiVersion: unknown;
stellarNetwork: unknown;
backendUrl: unknown;
frontendUrl: unknown;
};
featureFlags: {
responseTiming: unknown;
apiVersionHeader: unknown;
schemaVersionHeader: unknown;
requestLogging: unknown;
indexerDedupe: unknown;
indexerDlq: unknown;
indexerCursorStalenessWarning: unknown;
ownershipSnapshotCleanup: unknown;
};
}

/**
* Build the structured startup configuration summary.
*
* @example
* import { logger } from './utils/logger.utils';
* import { buildStartupConfigSummary } from './utils/config-summary.utils';
*
* logger.info(buildStartupConfigSummary(), 'Loaded runtime configuration');
*/
export function buildStartupConfigSummary(): StartupConfigSummary {
const config = maskSensitiveConfigValues();

return {
environment: {
mode: config.MODE,
port: config.PORT,
apiVersion: config.API_VERSION,
stellarNetwork: config.STELLAR_NETWORK,
backendUrl: config.BACKEND_URL,
frontendUrl: config.FRONTEND_URL,
},
featureFlags: {
responseTiming: config.ENABLE_RESPONSE_TIMING,
apiVersionHeader: config.ENABLE_API_VERSION_HEADER,
schemaVersionHeader: config.ENABLE_SCHEMA_VERSION_HEADER,
requestLogging: config.ENABLE_REQUEST_LOGGING,
indexerDedupe: config.ENABLE_INDEXER_DEDUPE,
indexerDlq: config.ENABLE_INDEXER_DLQ,
indexerCursorStalenessWarning:
config.ENABLE_INDEXER_CURSOR_STALENESS_WARNING,
ownershipSnapshotCleanup: config.OWNERSHIP_SNAPSHOT_CLEANUP_ENABLED,
},
};
}
Loading