> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Configure your application with environment variables and roles — both declared in YAML and loaded at boot.

An Ooneex application is configured through two YAML files in the `shared` module:

* **`modules/shared/.env.yml`** — environment variables: database URLs, API keys, ports, CORS, and per-environment access lists.
* **`modules/shared/src/roles.yml`** — the role hierarchy your [controllers](/controller) check for access control.

Both are generated by [`app:create`](/cli/commands/app-create) and loaded automatically when the app boots. This page covers how to edit them and read their values in code.

Alongside them, `app:create` writes a set of **project-tooling** files at the repository root — commit conventions, code style, TypeScript, and editor settings. They are scaffolded with sensible defaults so the toolchain works out of the box; the sections at the end of this page describe each one and how to tune it.

## Environment variables

Environment is configured in YAML rather than a flat `.env` file. At boot, [`@ooneex/app-env`](/components/auth) reads `modules/shared/.env.yml`, flattens its nested keys into uppercase environment variables (`app.port` → `PORT`, `database.url` → `DATABASE_URL`, `ai.anthropic.api_key` → `ANTHROPIC_API_KEY`), and exposes them through a typed, injectable `AppEnv` class. Empty values are skipped, so unconfigured services stay unset.

### Installation

Included in every scaffolded project. To add it manually:

```bash theme={null}
bun add @ooneex/app-env
```

### How to use

Edit `modules/shared/.env.yml`. The file is organized into sections — one per concern. Fill in the services you use and leave the rest empty.

```yaml modules/shared/.env.yml theme={null}
app:
  env: "development" # development | staging | production | local | ...
  host: "127.0.0.1"
  port: 3000

database:
  url: "postgresql://user:password@localhost:5432/movie_app"

cache:
  redis:
    url: "redis://localhost:6379"

jwt:
  secret: "change-me-in-production"

mailer:
  sender:
    name: "Movie App"
    address: "noreply@movie-app.com"
  resend:
    api_key: ""

ai:
  anthropic:
    api_key: "sk-ant-..."

# Comma-separated emails granted elevated roles per environment
allowed_users:
  development: "dev@example.com,qa@example.com"
  super_admin: "founder@example.com"
  admin: "admin@example.com"
```

The `app.env` value drives environment detection. Supported values include `local`, `development`, `staging`, `testing`, `qa`, `uat`, `preview`, `demo`, `sandbox`, `beta`, `canary`, `hotfix`, and `production`.

<Note>
  Treat `.env.yml` as a secret. Keep real credentials out of version control and
  inject them per environment in CI/CD and production.
</Note>

### Reading values in code

`AppEnv` is registered in the container at boot. Inject it into any class with `@inject` and read its typed properties:

```typescript theme={null}
import { AppEnv } from "@ooneex/app-env";
import { inject } from "@ooneex/container";
import { decorator } from "@ooneex/service";

@decorator.service()
export class CatalogService {
  constructor(@inject(AppEnv) private readonly env: AppEnv) {}

  public async execute() {
    this.env.APP_ENV; // "development"
    this.env.PORT; // 3000
    this.env.DATABASE_URL; // "postgresql://..."
    this.env.isProduction; // boolean
    this.env.isLocal; // boolean
  }
}
```

Inside a [controller](/controller), the same values are available on the context as `context.env`.

## Roles

Roles are an access-control hierarchy declared in `modules/shared/src/roles.yml` and enforced by [`@ooneex/role`](/components/permission). Each role can inherit from a parent, so a user with `ROLE_ADMIN` automatically satisfies any route requiring `ROLE_USER`. A [controller](/controller) restricts access by listing roles on its route — `roles: ["ROLE_USER"]` — and the framework rejects requests from users who lack a matching (or inheriting) role with `403 Forbidden`.

At boot the app validates the file, registers it in the container, and — in local development — regenerates `roles.types.ts` so role names are type-checked.

### Installation

Included in every scaffolded project. To add it manually:

```bash theme={null}
bun add @ooneex/role
```

### How to use

Edit `modules/shared/src/roles.yml`. The `roles` section maps friendly names to role keys; the `hierarchy` section declares each role's inheritance and description.

```yaml modules/shared/src/roles.yml theme={null}
roles:
  GUEST: ROLE_GUEST
  USER: ROLE_USER
  PREMIUM_USER: ROLE_PREMIUM_USER
  MODERATOR: ROLE_MODERATOR
  ADMIN: ROLE_ADMIN
  SUPER_ADMIN: ROLE_SUPER_ADMIN

hierarchy:
  ROLE_GUEST:
    description: Unauthenticated visitor with read-only access to public content

  ROLE_USER:
    inherits: [ROLE_GUEST]
    description: Standard authenticated user with full access to core features

  ROLE_PREMIUM_USER:
    inherits: [ROLE_USER]
    description: Paid subscriber with access to premium features

  ROLE_MODERATOR:
    inherits: [ROLE_USER]
    description: Community moderator who can manage posts and reports

  ROLE_ADMIN:
    inherits: [ROLE_MODERATOR]
    description: Administrator with full control over users and content

  ROLE_SUPER_ADMIN:
    inherits: [ROLE_ADMIN]
    description: Super administrator with unrestricted access
```

The default scaffold ships a richer hierarchy (`ROLE_TRIAL_USER`, `ROLE_VIP_USER`, `ROLE_REVIEWER`, `ROLE_MANAGER`, `ROLE_SYSTEM`, …). Keep what you need and prune the rest — but always pick the **least-privileged** role that satisfies an endpoint.

### Restricting a route

List the required role on the controller's route. Inheritance means higher roles pass automatically:

```typescript theme={null}
@Route.get("/movies", {
  name: "movie.list",
  version: 1,
  description: "List movies",
  roles: ["ROLE_USER"], // ROLE_ADMIN, ROLE_SUPER_ADMIN, … also pass
})
export class MovieListController {
  public async index(context: ContextType<MovieListRouteType>) {
    const user = context.user; // the authenticated user and its roles
    return context.response.json({
      /* ... */
    });
  }
}
```

### Granting roles by email

The `allowed_users` section of `.env.yml` grants elevated roles to specific emails per environment. When an authenticated user's email matches, the framework adds the corresponding role at request time — handy for seeding the first `ADMIN` or `SUPER_ADMIN` without a database migration:

```yaml modules/shared/.env.yml theme={null}
allowed_users:
  super_admin: "founder@example.com"
  admin: "admin@example.com,ops@example.com"
```

## Commit conventions

Commits are linted with [commitlint](https://commitlint.js.org/) so history stays consistent and machine-readable. `app:init` generates `.commitlintrc.ts` at the project root, wires it into [Husky](https://typicode.github.io/husky/) Git hooks, and runs [lint-staged](https://github.com/lint-staged/lint-staged) on staged files before each commit.

Three pieces work together:

* **`.husky/pre-commit`** runs `lint-staged`, which runs Biome over staged `.js/.ts/.jsx/.tsx/.json/.jsonc` files.
* **`.husky/commit-msg`** runs `bunx commitlint --edit "$1"` to validate the message.
* **`.commitlintrc.ts`** declares the rules and the interactive `bunx commit` prompt.

### How to use

Write [Conventional Commits](https://www.conventionalcommits.org/) — `type(scope): subject`. The config enforces the allowed types and scopes:

```typescript .commitlintrc.ts theme={null}
import { RuleConfigSeverity, type UserConfig } from "@commitlint/types";

const Configuration: UserConfig = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [
      RuleConfigSeverity.Error,
      "always",
      ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"],
    ],
    "scope-enum": [
      RuleConfigSeverity.Error,
      "always",
      ["common", "shared", "app"],
    ],
    "scope-empty": [RuleConfigSeverity.Error, "never"],
    "subject-empty": [RuleConfigSeverity.Error, "never"],
    "header-max-length": [RuleConfigSeverity.Error, "always", 100],
    // ...
  },
};

export default Configuration;
```

A scope is **required** and must be one of `common`, `shared`, or `app`. Add a new module's name to `scope-enum` when you create it, so commits like `feat(billing): add invoices` pass. Multiple scopes are allowed (`feat(app, shared): ...`).

Instead of `git commit`, you can run the interactive prompt:

```bash theme={null}
bun run commit
```

It walks you through the type, scope, and subject and produces a compliant message.

## Code style

Formatting and linting are handled by [Biome](https://biomejs.dev/) — a single fast tool that replaces ESLint and Prettier. `app:init` writes `biome.jsonc` at the project root.

### How to use

Format and auto-fix the whole project, or lint without writing:

```bash theme={null}
bun run fmt    # biome check --write
bun run lint   # runs lint across every module
```

The generated `biome.jsonc` enables both the formatter and a strict linter, and turns on import organization:

```jsonc biome.jsonc theme={null}
{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 120,
    "lineEnding": "lf"
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "error",
        "noConsole": "error"
      },
      "style": {
        "useImportType": "error",
        "useTemplate": "error"
      }
    }
  },
  "javascript": {
    "formatter": { "quoteStyle": "double" },
    "parser": { "unsafeParameterDecoratorsEnabled": true }
  },
  "assist": {
    "enabled": true,
    "actions": { "source": { "organizeImports": "on" } }
  }
}
```

Key defaults worth knowing:

* **2-space indent, 120-column lines, LF endings, double quotes.**
* `noExplicitAny` and `noConsole` are **errors** — the linter rejects `any` and stray `console.*` calls.
* `unsafeParameterDecoratorsEnabled` is on so the framework's `@inject(...)` parameter decorators parse.
* Imports are organized automatically on format.

Adjust any rule by editing `biome.jsonc`; see the [Biome rules reference](https://biomejs.dev/linter/rules/) for the full list.

## TypeScript

`tsconfig.json` at the project root sets strict compiler options and the module path aliases used across the app.

### How to use

The defaults are strict and Bun-targeted — most projects never touch them:

```jsonc tsconfig.json theme={null}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "jsx": "react-jsx",
    "paths": {
      "@module/app/*": ["./modules/app/src/*"],
      "@module/shared/*": ["./modules/shared/src/*"]
    },
    "types": ["bun"]
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", ".github", ".husky", ".nx", ".zed", ".vscode"]
}
```

Note:

* `experimentalDecorators` and `emitDecoratorMetadata` are required for the framework's `@decorator.*` and `@inject` decorators.
* The `paths` aliases let you import across modules with `@module/<module>/...` instead of relative paths. **Add an entry for each new module** so its imports resolve.
* Strict flags like `noUncheckedIndexedAccess` and `noUnusedLocals` are on by default — keep them on for the strongest type safety.

## Editor (Zed)

`app:init` writes `.zed/settings.json` so the [Zed editor](https://zed.dev/) formats and fixes with Biome on save out of the box.

### How to use

Open the project in Zed — no extra setup. The generated settings make Biome the formatter for JS, TS, TSX, JSON, JSONC, CSS, Astro, and Svelte, and run Biome's fix + organize-imports actions on every save:

```json .zed/settings.json theme={null}
{
  "formatter": "language_server",
  "format_on_save": "on",
  "languages": {
    "TypeScript": {
      "formatter": { "language_server": { "name": "biome" } },
      "code_actions_on_format": {
        "source.fixAll.biome": true,
        "source.organizeImports.biome": true
      }
    }
    // ...same for JavaScript, TSX, JSON, JSONC, CSS, Astro, Svelte
  }
}
```

This mirrors the `bun run fmt` behavior on save, so files are formatted and imports organized as you work. Using a different editor? Point its Biome integration at `biome.jsonc` for the same result — VS Code via the [Biome extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome), or any editor with an LSP client.

## Use with Claude and Codex

Initialize the AI skills, then ask your agent to wire configuration in natural language — it edits the YAML files and uses the project's actual roles.

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    ```text Prompt icon="terminal" wrap theme={null}
    Configure the app for local development: set the Postgres and Redis URLs
    in .env.yml, add a PREMIUM_USER role inheriting from USER in roles.yml,
    and grant admin@example.com the ADMIN role.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    ```text Prompt icon="terminal" wrap theme={null}
    Configure the app for local development: set the Postgres and Redis URLs
    in .env.yml, add a PREMIUM_USER role inheriting from USER in roles.yml,
    and grant admin@example.com the ADMIN role.
    ```
  </Tab>
</Tabs>

## External resources

<CardGroup cols={2}>
  <Card title="YAML Spec" icon={<Icon icon="file-code" size={16} />} href="https://yaml.org/spec/">
    The format both configuration files use.
  </Card>

  <Card title="PostgreSQL Connection URLs" icon={<Icon icon="database" size={16} />} href="https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING">
    Format for the `database.url` value.
  </Card>

  <Card title="Redis" icon={<Icon icon="bolt" size={16} />} href="https://redis.io/docs/latest/">
    Cache, queue, and pub/sub connection URLs.
  </Card>

  <Card title="The Twelve-Factor App: Config" icon={<Icon icon="gear" size={16} />} href="https://12factor.net/config">
    Why configuration lives in the environment.
  </Card>
</CardGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Controller" icon={<Icon icon="route" size={16} />} href="/controller">
    Use the roles you defined to restrict your endpoints.
  </Card>

  <Card title="Auth component reference" icon={<Icon icon="lock" size={16} />} href="/components/auth">
    The full `@ooneex/app-env` environment API.
  </Card>

  <Card title="Permission component reference" icon={<Icon icon="user-shield" size={16} />} href="/components/permission">
    Fine-grained access checks beyond roles.
  </Card>

  <Card title="Create your app" icon={<Icon icon="rocket" size={16} />} href="/getting-started/create-app">
    Where these files are generated.
  </Card>
</CardGroup>
