Skip to content

Template System

FORGE uses the Tera template engine to generate source code. Every file in a generated project originates from a .tera template that is processed with variables derived from the project's forge.yaml configuration. This architecture makes FORGE's output fully deterministic: the same configuration always produces the same code.

How Templates Work

When you run forge new or any forge make:* command, FORGE follows this pipeline:

  1. Read configuration from forge.yaml (app name, features, languages, providers)
  2. Build a context object containing all template variables
  3. Locate the relevant .tera templates from the templates directory
  4. Render each template through the Tera engine, replacing variables and evaluating conditionals
  5. Write the output to the project directory with the .tera extension stripped
forge.yaml ──→ Context Variables ──→ Tera Engine ──→ Generated Code

                                    .tera Templates

Template Directory Structure

Templates are organized by backend framework and frontend framework, with shared templates for cross-cutting concerns:

templates/
├── backends/
│   ├── rust/                    ← Rust + Axum (default backend)
│   │   ├── manifest.yaml
│   │   ├── api/
│   │   │   ├── Cargo.toml.tera
│   │   │   └── src/
│   │   │       ├── main.rs.tera
│   │   │       ├── config/
│   │   │       ├── models/
│   │   │       ├── handlers/
│   │   │       │   ├── auth.rs.tera
│   │   │       │   ├── profile.rs.tera
│   │   │       │   └── admin/
│   │   │       │       ├── users.rs.tera
│   │   │       │       ├── contents.rs.tera
│   │   │       │       └── menus.rs.tera
│   │   │       ├── middleware/
│   │   │       ├── routes/
│   │   │       ├── services/
│   │   │       ├── dto/
│   │   │       ├── providers/
│   │   │       │   ├── sms/
│   │   │       │   ├── email/
│   │   │       │   ├── storage/
│   │   │       │   └── payments/
│   │   │       ├── error/
│   │   │       └── utils/
│   │   └── migrations/
│   │       ├── 00001_create_languages_table.sql.tera
│   │       ├── 00002_create_users_table.sql.tera
│   │       └── ... (17 migration templates)
│   ├── laravel/
│   ├── fastapi/
│   └── node/

├── frontends/
│   ├── nextjs/                  ← Next.js (default frontend)
│   │   ├── web/                 ← Public-facing web app
│   │   │   ├── app/
│   │   │   │   ├── layout.tsx.tera
│   │   │   │   ├── page.tsx.tera
│   │   │   │   ├── (auth)/
│   │   │   │   ├── (protected)/
│   │   │   │   └── [slug]/
│   │   │   ├── components/
│   │   │   │   ├── site-header.tsx.tera
│   │   │   │   ├── site-footer.tsx.tera
│   │   │   │   ├── dynamic-nav.tsx.tera
│   │   │   │   ├── language-switcher.tsx.tera
│   │   │   │   └── providers/
│   │   │   └── lib/
│   │   │       ├── api.ts.tera
│   │   │       └── types.ts.tera
│   │   └── admin/               ← Admin dashboard
│   │       ├── app/
│   │       │   ├── (dashboard)/
│   │       │   │   ├── users/
│   │       │   │   ├── contents/
│   │       │   │   ├── menus/
│   │       │   │   └── settings/
│   │       └── components/
│   ├── nuxtjs/
│   ├── angular/
│   └── vanilla/

├── modules/                     ← Optional feature modules
│   ├── crm/
│   └── helpdesk/

└── shared/                      ← Cross-cutting templates
    ├── config/
    │   ├── forge.yaml.tera
    │   └── env.example.tera
    ├── infra/
    │   ├── docker/
    │   └── caddy/
    └── database/
        └── seeders/

File Naming Convention

The .tera extension signals that a file is a template:

File PatternMeaning
main.rs.teraRust source template, outputs main.rs
page.tsx.teraTypeScript React template, outputs page.tsx
00002_create_users_table.sql.teraSQL migration template
Cargo.toml.teraTOML config template
docker-compose.yml.teraYAML config template
README.md.teraMarkdown template

Files without the .tera extension are copied as-is without processing.

Template Variables

Every template receives a standard context built from forge.yaml. Here is the complete set of variables available:

Application Variables

yaml
app:
  name: "myapp"                    # {{ app.name }}
  display_name: "My Application"   # {{ app.display_name }}
  description: "A FORGE app"       # {{ app.description }}
  version: "1.0.0"                 # {{ app.version }}

Backend & Frontend

yaml
backend:
  framework: "rust"                # {{ backend.framework }}
  port: 8080                       # {{ backend.port }}

frontend:
  framework: "nextjs"              # {{ frontend.framework }}
  web_port: 3000                   # {{ frontend.web_port }}
  admin_port: 3001                 # {{ frontend.admin_port }}

Database

yaml
database:
  driver: "postgresql"             # {{ database.driver }}
  host: "localhost"                # {{ database.host }}
  port: 5432                       # {{ database.port }}
  name: "myapp"                    # {{ database.name }}

Domains

yaml
domains:
  web: "myapp.test"                # {{ domains.web }}
  admin: "admin.myapp.test"        # {{ domains.admin }}
  api: "api.myapp.test"            # {{ domains.api }}

Authentication

yaml
auth:
  method: "email_password"         # {{ auth.method }}
  jwt_expiry: "60m"                # {{ auth.jwt_expiry }}

Languages

yaml
languages:
  default: "en"                    # {{ languages.default }}
  list:                            # {% for lang in languages.list %}
    - code: "en"
      name: "English"
      direction: "ltr"
    - code: "ar"
      name: "Arabic"
      direction: "rtl"

Providers

yaml
providers:
  sms: "twilio"                    # {{ providers.sms }}
  email: "smtp"                    # {{ providers.email }}
  storage: "local"                 # {{ providers.storage }}

FORGE Metadata

yaml
forge:
  version: "2.0.0"                 # {{ forge.version }}
  generated_at: "2026-01-21"       # {{ forge.generated_at }}

Custom Filters

FORGE registers custom Tera filters for common string transformations:

{{ app.name | snake_case }}        → my_app
{{ app.name | pascal_case }}       → MyApp
{{ app.name | kebab_case }}        → my-app
{{ app.name | camel_case }}        → myApp
{{ app.name | upper }}             → MYAPP

Conditional Blocks

Templates use Tera's control flow to include or exclude code based on configuration. This is how a single template set produces different output for different projects.

Feature Conditionals

rust
// main.rs.tera

use {{ app.name | snake_case }}::config::AppConfig;
use {{ app.name | snake_case }}::routes;

{% if feature_content %}
use {{ app.name | snake_case }}::handlers::contents;
{% endif %}

{% if feature_menus %}
use {{ app.name | snake_case }}::handlers::menus;
{% endif %}

Authentication Method

rust
// handlers/auth.rs.tera

{% if auth.method == "email_password" %}
pub async fn login(
    Json(payload): Json<LoginRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
    let user = find_by_email(&payload.email).await?;
    verify_password(&payload.password, &user.password)?;
    let token = generate_jwt(&user)?;
    Ok(Json(AuthResponse { user, token }))
}
{% elif auth.method == "mobile_otp" %}
pub async fn request_otp(
    Json(payload): Json<OtpRequest>,
) -> Result<Json<MessageResponse>, ApiError> {
    let code = generate_otp();
    store_otp(&payload.phone, &code).await?;
    sms_provider.send_otp(&payload.phone, &code).await?;
    Ok(Json(MessageResponse { message: "OTP sent" }))
}
{% endif %}

Language-Aware Templates

tsx
// layout.tsx.tera

{% if languages.list | length > 1 %}
import { LanguageSwitcher } from '@/components/language-switcher'
{% endif %}

export default function RootLayout({ children }) {
  return (
    <html lang="{{ languages.default }}"
      {%- if languages.list | filter(attribute="direction", value="rtl") | length > 0 %}
        dir={direction}
      {%- endif %}>
      <body>
        {% if languages.list | length > 1 %}
        <LanguageSwitcher />
        {% endif %}
        {children}
      </body>
    </html>
  )
}

Provider Selection

rust
// providers/sms/mod.rs.tera

{% if providers.sms == "twilio" %}
mod twilio;
pub use twilio::TwilioProvider as SmsDriver;
{% elif providers.sms == "unifonic" %}
mod unifonic;
pub use unifonic::UnifonicProvider as SmsDriver;
{% else %}
mod log_provider;
pub use log_provider::LogProvider as SmsDriver;
{% endif %}

Template Example: Full Walkthrough

Here is a complete example showing a migration template and what it produces.

Input Template

sql
-- 00002_create_users_table.sql.tera

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    {% if auth.method == "mobile_otp" %}
    phone VARCHAR(50) UNIQUE NOT NULL,
    {% else %}
    phone VARCHAR(50) UNIQUE,
    {% endif %}
    password VARCHAR(255) NOT NULL,
    avatar VARCHAR(500),
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
{% if auth.method == "mobile_otp" %}
CREATE INDEX idx_users_phone ON users(phone);
{% endif %}

Configuration

yaml
# forge.yaml
auth:
  method: "email_password"

Generated Output

sql
-- 00002_create_users_table.sql

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    phone VARCHAR(50) UNIQUE,
    password VARCHAR(255) NOT NULL,
    avatar VARCHAR(500),
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

Notice that the phone column is optional (no NOT NULL) and the phone index is omitted because the auth method is email_password, not mobile_otp.

Iteration in Templates

Templates frequently iterate over collections, such as generating code for each configured language:

sql
-- seeders/01_languages.sql.tera

INSERT INTO languages (code, name, direction, is_default, is_active) VALUES
{% for lang in languages.list %}
    ('{{ lang.code }}', '{{ lang.name }}', '{{ lang.direction }}',
     {{ lang.code == languages.default }}, true)
    {%- if not loop.last %},{% endif %}
{% endfor %};

With two languages configured, this produces:

sql
INSERT INTO languages (code, name, direction, is_default, is_active) VALUES
    ('en', 'English', 'ltr', true, true),
    ('ar', 'Arabic', 'rtl', false, true);

Template Loading

FORGE supports two modes for loading templates:

File-Based (Development)

During development, templates are read from the filesystem:

rust
let engine = TemplateEngine::new(Path::new("templates/backends/rust"))?;

This allows rapid iteration on templates without recompiling the CLI.

Embedded (Production)

In production builds, all templates are compiled into the binary using rust-embed:

rust
use rust_embed::RustEmbed;

#[derive(RustEmbed)]
#[folder = "templates/"]
struct Templates;

This makes the forge CLI a single self-contained binary with no external dependencies. Users can still override embedded templates by placing custom versions in ~/.forge/templates/.

TIP

If you need to customize a specific template for your organization, copy it to ~/.forge/templates/ with the same relative path. FORGE checks this directory first before falling back to the embedded templates.

Template Manifest

Each backend and frontend has a manifest.yaml that tells FORGE which templates to process, what directories to create, and what post-generation hooks to run:

yaml
# templates/backends/rust/manifest.yaml

name: rust
display_name: Rust (Axum)
version: 2.0.0

forge_compatibility:
  minimum: "2.0.0"
  maximum: "3.x.x"

files:
  - source: "api/Cargo.toml.tera"
    destination: "apps/api/Cargo.toml"
    template: true

  - source: "api/src/main.rs.tera"
    destination: "apps/api/src/main.rs"
    template: true

  - source: "api/rust-toolchain.toml"
    destination: "apps/api/rust-toolchain.toml"
    template: false    # copied as-is

directories:
  - "apps/api/src/config"
  - "apps/api/src/handlers"
  - "apps/api/src/models"
  - "apps/api/src/middleware"
  - "apps/api/src/routes"
  - "apps/api/src/services"
  - "apps/api/src/dto"
  - "apps/api/src/error"
  - "apps/api/src/utils"

hooks:
  post_generate:
    - "cd apps/api && cargo check"

WARNING

Do not manually edit files marked as managed in the manifest. FORGE may overwrite them during upgrades. Place custom logic in core files (such as services) that FORGE will never modify.

Released under the MIT License.