Essentials
Configuration
Configuration lives on one manifest, the Blueprint, addressed by dotted stone.*keys. It is assembled once before the first event, so behaviour depends on what the manifest says, never on when a value happened to be set.
#Setting configuration
Pass options to @StoneApp (or into defineStoneApp). Keep your own settings under an app namespace so they never collide with framework or module keys.
import { StoneApp } from '@stone-js/core'
@StoneApp({
name: 'tasks',
// Your own namespace under stone.* is yours to use.
tasks: { pageSize: 20, allowGuests: false }
})
export class Application {}import { defineStoneApp } from '@stone-js/core'
export const appConfig = {
stone: { name: 'tasks', tasks: { pageSize: 20, allowGuests: false } }
}
export const App = defineStoneApp(appConfig, [/* blueprints */])#Configuration classes
Options on @StoneApp are literals: they say what a value is, not how to work it out. Reach for a configuration class as soon as a setting has to be computed, read from the environment, merged from a remote overlay, or as soon as you need to register blueprint middleware. It receives the Blueprint and writes onto it, before any event exists.
import { Configuration, IBlueprint } from '@stone-js/core'
@Configuration()
export class AppConfiguration {
configure (blueprint: IBlueprint): void {
blueprint.set('stone.tasks.pageSize', 20)
// Anything you can compute, you can configure: read the environment, merge a remote
// overlay, register blueprint middleware, await a source.
}
}import { defineConfig, IBlueprint } from '@stone-js/core'
export const AppConfiguration = defineConfig((blueprint: IBlueprint) => {
blueprint.set('stone.tasks.pageSize', 20)
})Both forms are the same thing: a module carrying a configure function, found by the same scan. Export it from your app directory and it runs; nothing registers it by hand.
#Ordering several configurations
A real application has more than one: static settings, a remote overlay (SSM, Secrets Manager), one per vendable module. Give them a priority when one depends on what another loads. It is ascending, the lowest runs first, and equal priorities keep their declaration order, so configurations that declare nothing behave exactly as before.
import { Configuration, ConfigurationPriority } from '@stone-js/core'
@Configuration({ priority: ConfigurationPriority.Sources }) // 0: everything may depend on these
export class RemoteConfiguration {
async configure (blueprint) {
await loadConfigSources(blueprint, [ssmSource({ path: '/my-app/' })])
}
}
@Configuration({ priority: ConfigurationPriority.App }) // 10: the default
export class AppConfiguration {
configure (blueprint) {
blueprint.set('stone.tasks.pageSize', blueprint.get('remote.pageSize', 20))
}
}import { defineConfig, ConfigurationPriority } from '@stone-js/core'
export const RemoteConfiguration = defineConfig(
async (blueprint) => await loadConfigSources(blueprint, [ssmSource({ path: '/my-app/' })]),
{ priority: ConfigurationPriority.Sources } // 0: everything may depend on these
)
export const AppConfiguration = defineConfig(
(blueprint) => blueprint.set('stone.tasks.pageSize', blueprint.get('remote.pageSize', 20)),
{ priority: ConfigurationPriority.App } // 10: the default
)The named steps are Sources (0), App (10) and Module (20), with gaps left so you can slot something between two of them without renumbering.
#Adjusting once everything is in place
An afterConfigure method runs after every configuration has been applied, not just yours. Use it to settle a value that depends on what modules ended up declaring, instead of guessing a priority high enough to come last.
configure (blueprint) {
blueprint.set('stone.tasks.pageSize', 20)
}
afterConfigure (blueprint) {
// Every module has declared its middleware by now, so this can react to the final list.
blueprint.set('stone.tasks.strict', blueprint.get('stone.kernel.middleware', []).length > 0)
}#Configuration that changes per event
A configuration marked live is not applied once at startup: the kernel runs it again for each incoming event. That is the deliberate exception to a resolved-once manifest, and it costs work on every request, so keep it for values that genuinely move (a feature-flag service, a per-tenant overlay) rather than for convenience.
@Configuration({ live: true })
export class FlagsConfiguration {
async configure (blueprint) {
blueprint.set('stone.tasks.flags', await fetchFlags())
}
}#Reading configuration
The config store is injected like any service and read by the same dotted keys, always with a default so a missing key never surprises you.
constructor ({ config }) {
this.pageSize = config.get('stone.tasks.pageSize', 20)
this.allowGuests = config.get<boolean>('stone.tasks.allowGuests', false)
}#Environment-driven config
Values that differ per deployment come from the environment, read through typed getters and folded into the manifest, so the domain never reads process.env directly.
import { getNumber } from '@stone-js/env'
export const appConfig = {
stone: { tasks: { pageSize: getNumber('PAGE_SIZE', 20) } }
}