Stone.jsDocs
Paradigm

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.

app/Application.tsdeclarativeimperative
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.

app/configurations/AppConfiguration.tsdeclarativeimperative
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.

app/configurations/RemoteConfiguration.tsdeclarativeimperative
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.

app/configurations/AppConfiguration.tsts
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.

app/configurations/FlagsConfiguration.tsts
@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.

app/Tasks.tsts
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.

app/config.tsts
import { getNumber } from '@stone-js/env'

export const appConfig = {
  stone: { tasks: { pageSize: getNumber('PAGE_SIZE', 20) } }
}

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)