Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
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
24 changes: 23 additions & 1 deletion astro.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export default defineConfig({
{ label: 'Versioning', slug: 'core-concepts/versioning' },
{ label: 'Dependency Injection', slug: 'core-concepts/dependency-injection' },
{ label: 'Providers', slug: 'core-concepts/providers' },
{ label: 'Macroable', slug: 'core-concepts/macroable' },
{ label: 'Events', slug: 'core-concepts/events' },
{ label: 'Lifecycle Hooks', slug: 'core-concepts/lifecycle-hooks' },
{ label: 'Configuration', slug: 'core-concepts/configuration' },
Expand All@@ -80,8 +81,12 @@ export default defineConfig({
{ label: 'Validation', slug: 'guides/validation' },
{ label: 'Guards', slug: 'guides/guards' },
{ label: 'Middleware', slug: 'guides/middleware' },
{ label: 'Rate Limiting', slug: 'guides/rate-limiting' },
{ label: 'Error Handling', slug: 'guides/error-handling' },
{ label: 'Environment Typing', slug: 'guides/environment-typing' },
{ label: 'Domain Routing', slug: 'guides/domain-routing' },
{ label: 'Signed URLs', slug: 'guides/signed-urls' },
{ label: 'Streaming Responses', slug: 'guides/streaming' },
],
},
{
Expand All@@ -90,6 +95,7 @@ export default defineConfig({
{ label: 'Queues', slug: 'integrations/queues' },
{ label: 'Cron Jobs', slug: 'integrations/cron-jobs' },
{ label: 'Caching', slug: 'integrations/caching' },
{ label: 'Feature Flags', slug: 'integrations/feature-flags' },
{ label: 'Storage', slug: 'integrations/storage' },
{ label: 'Email', slug: 'integrations/email' },
{ label: 'Internationalization', slug: 'integrations/i18n' },
Expand DownExpand Up@@ -121,10 +127,26 @@ export default defineConfig({
{ label: 'Seeders', slug: 'framework/seeders' },
{ label: 'Factories', slug: 'framework/factories' },
{ label: 'Auth', slug: 'framework/auth' },
{ label: 'RBAC', slug: 'framework/rbac' },
{ label: 'Access Control', slug: 'framework/access-control' },
{ label: 'Auth Guard', slug: 'framework/auth-guard' },
],
},
{
label: '@stratal/inertia',
items: [
{ label: 'Overview & Setup', slug: 'inertia/overview' },
{ label: 'Pages & Rendering', slug: 'inertia/pages-and-rendering' },
{ label: 'Shared Data & Props', slug: 'inertia/shared-data-and-props' },
{ label: 'Flash Messages', slug: 'inertia/flash-messages' },
{ label: 'SSR', slug: 'inertia/ssr' },
{ label: 'Forms & Validation', slug: 'inertia/forms-and-validation' },
{ label: 'Modals', slug: 'inertia/modals' },
{ label: 'React Hooks', slug: 'inertia/react-hooks' },
{ label: 'Vite Plugin', slug: 'inertia/vite-plugin' },
{ label: 'Testing', slug: 'inertia/testing' },
{ label: 'CLI Commands', slug: 'inertia/cli-commands' },
],
},
{
label: 'API Reference',
attrs: { target: '_blank' },
Expand Down
45 changes: 35 additions & 10 deletions src/content/docs/core-concepts/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,9 @@ title: Configuration
description: Typed config namespaces, dot-notation access, schema validation, and runtime overrides with registerAs and ConfigService.
---

import { Aside } from '@astrojs/starlight/components';
import { Aside, LinkCard } from '@astrojs/starlight/components';

The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.
The configuration system gives you a structured, type-safe way to manage application settings in Stratal. It is environment-agnostic - the framework never reads environment variables directly. Instead, your application defines **config namespaces** that extract values from the Cloudflare `Env` object and expose them through a centralized `ConfigService`.

Key capabilities:

Expand All@@ -14,13 +14,38 @@ Key capabilities:
- Optional Zod schema validation at startup
- Runtime overrides via `config.set()` for request-scoped values

## Application config versus namespaced config

Two layers of configuration coexist, and they serve different purposes:

- **Application config** is the object you pass to the `Stratal` constructor. It tunes framework behaviour at boot: `versioning`, `trailingSlash`, `logging`, and the exception handler. The `trailingSlash` key accepts either a bare mode (`'ignore' | 'always' | 'never'`) or a `{ mode, exclude }` object that exempts specific paths from canonicalization.

```typescript
export default new Stratal({
module: AppModule,
trailingSlash: { mode: 'always', exclude: ['/auth/oauth2'] },
})
```

- **Namespaced config** is everything described on the rest of this page: your application's own settings, derived from the Cloudflare `Env` object through `registerAs()` and read back through `ConfigService`.

<Aside type="note">
Application config is static and read once at construction. Namespaced config supports schema validation and request-scoped runtime overrides. Reach for namespaced config for anything your application defines.
</Aside>

<LinkCard
title="Routing: trailing slashes and URL generation"
href="/core-concepts/controllers-and-routing/#trailing-slashes"
description="Full reference for the trailingSlash modes, exclusion patterns, and how generated URLs stay consistent with redirects."
/>

## How it works

Configuration flows through three steps:

1. **Define** create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** inject `ConfigService` anywhere and access values with dot-notation paths.
1. **Define** - create config namespaces with `registerAs()`, each receiving the `Env` object and returning a typed config object.
2. **Register** - pass all namespaces to `ConfigModule.forRoot()` in your root or shared module. The module resolves the Cloudflare `Env`, calls each factory, optionally validates the merged result, and initializes `ConfigService`.
3. **Use** - inject `ConfigService` anywhere and access values with dot-notation paths.

```mermaid
flowchart LR
Expand DownExpand Up@@ -101,7 +126,7 @@ export class CoreModule {}
| `validateSchema` | `ZodSchema` | No | A Zod schema to validate the merged config at startup |

<Aside type="note">
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it they can inject `ConfigService` directly since it is registered in the global DI container.
Import `ConfigModule.forRoot()` once in your root module or a shared `CoreModule`. Child feature modules do not need to import it - they can inject `ConfigService` directly since it is registered in the global DI container.
</Aside>

## Injecting and using ConfigService
Expand DownExpand Up@@ -166,7 +191,7 @@ declare module 'stratal' {
}
```

After this augmentation, `config.get('database.url')` is fully typed your editor will autocomplete valid paths and flag invalid ones at compile time.
After this augmentation, `config.get('database.url')` is fully typed - your editor will autocomplete valid paths and flag invalid ones at compile time.

## Schema validation

Expand DownExpand Up@@ -215,13 +240,13 @@ ConfigValidationError: Configuration validation failed
Use `config.set()` to override config values during a request. This is useful for middleware that adjusts settings based on request context:

```typescript
// In middleware override for this request
// In middleware - override for this request
async handle(ctx: RouterContext, next: () => Promise<void>) {
this.config.set('email.from.name', 'Custom Name')
await next()
}

// In a downstream service reflects the override
// In a downstream service - reflects the override
async sendEmail() {
const fromName = this.config.get('email.from.name') // 'Custom Name'
}
Expand DownExpand Up@@ -351,7 +376,7 @@ To add a new config namespace to your application:
})
```

4. **Set environment variables** add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).
4. **Set environment variables** - add to `.dev.vars.example` for local development and set in production via `wrangler secret put` (sensitive) or `wrangler.jsonc` (non-sensitive).

## Testing

Expand Down
Loading