Skip to main content
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 check for access control.
Both are generated by 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 reads modules/shared/.env.yml, flattens its nested keys into uppercase environment variables (app.portPORT, database.urlDATABASE_URL, ai.anthropic.api_keyANTHROPIC_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:
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.
modules/shared/.env.yml
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.
Treat .env.yml as a secret. Keep real credentials out of version control and inject them per environment in CI/CD and production.

Reading values in code

AppEnv is registered in the container at boot. Inject it into any class with @inject and read its typed properties:
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, 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. Each role can inherit from a parent, so a user with ROLE_ADMIN automatically satisfies any route requiring ROLE_USER. A 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:
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.
modules/shared/src/roles.yml
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:
@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:
modules/shared/.env.yml
allowed_users:
  super_admin: "founder@example.com"
  admin: "admin@example.com,ops@example.com"

Commit conventions

Commits are linted with commitlint so history stays consistent and machine-readable. app:init generates .commitlintrc.ts at the project root, wires it into Husky Git hooks, and runs 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 Commitstype(scope): subject. The config enforces the allowed types and scopes:
.commitlintrc.ts
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:
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 — 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:
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:
biome.jsonc
{
  "$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 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:
tsconfig.json
{
  "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 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:
.zed/settings.json
{
  "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, 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.
ooneex claude:init
Prompt
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.

External resources

YAML Spec

The format both configuration files use.

PostgreSQL Connection URLs

Format for the database.url value.

Redis

Cache, queue, and pub/sub connection URLs.

The Twelve-Factor App: Config

Why configuration lives in the environment.

Next steps

Controller

Use the roles you defined to restrict your endpoints.

Auth component reference

The full @ooneex/app-env environment API.

Permission component reference

Fine-grained access checks beyond roles.

Create your app

Where these files are generated.