use super::{Backend, Template};
pub fn cargo_toml(
name: &str,
template: Template,
backend: Backend,
features: &[String],
rustango_path: Option<&str>,
) -> String {
let rustango_dep = template.rustango_dep(rustango_path, features);
let default_backend = backend.feature();
let wants_jobs = template.base_features().iter().any(|f| *f == "batteries")
|| features
.iter()
.any(|f| f == "jobs" || f == "jobs-postgres" || f == "batteries");
let async_trait_dep = if wants_jobs {
"\n# `rustango::jobs::Job` is an async trait — implementing one needs this.\n\
async-trait = \"0.1\""
} else {
""
};
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
# Empty `[workspace]` table makes this project standalone: if a parent
# directory has its own workspace `Cargo.toml`, cargo would otherwise
# refuse to build (see "current package believes it's in a workspace
# when it's not"). This declaration severs that link without taking on
# any workspace members. Delete it if you intentionally want the
# project to be a member of a parent workspace.
[workspace]
[dependencies]
# `rustango` re-exports every proc-macro (Model / Form / ViewSet /
# Serializer / embed_migrations / main), so you do NOT need to depend
# on `rustango-macros` directly. Use `rustango::Model` etc.
rustango = {rustango_dep}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread", "sync", "signal", "net"] }}
axum = {{ version = "0.8", default-features = false, features = ["tokio", "http1", "json", "form", "query"] }}
tower = {{ version = "0.5", features = ["util"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
chrono = {{ version = "0.4", default-features = false, features = ["serde", "clock"] }}
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
dotenvy = "0.15"{async_trait_dep}
[dev-dependencies]
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
# The `#[derive(Model)]` FromRow / LoadRelated impls are
# `#[cfg(feature = "...")]`-gated on THIS crate's features, so a backend
# feature must be enabled here for them to compile. `default` picks the
# one `cargo run` uses; switch with e.g.
# `cargo run --no-default-features --features sqlite`.
[features]
default = ["{default_backend}"]
postgres = ["rustango/postgres"]
sqlite = ["rustango/sqlite"]
mysql = ["rustango/mysql"]
"#
)
}
pub fn env_example(name: &str, backend: Backend) -> String {
let url = backend.compose_url(name);
let host_note = match backend.service() {
Some(svc) => format!(
"# Defaults suit `docker compose up -d`; the database name matches
# docker-compose.yml. Running cargo on the host instead of in the rust
# container? Change `{svc}` -> `localhost` below."
),
None => "# SQLite needs no database server — the file lives beside the
# project and is created on first `cargo run -- migrate`."
.to_owned(),
};
format!(
"# Copy to .env and edit for your environment; src/main.rs loads it at startup.
{host_note}
DATABASE_URL={url}
RUSTANGO_BIND=0.0.0.0:8080
# Apex domain the operator console is served on.
RUSTANGO_APEX_DOMAIN=localhost
# Session signing key — 32 bytes of base64. Anything else is discarded
# silently and a key is generated into ./var/ instead, so a project that
# looks configured is not. Leave it commented in development; set a real
# one in production and keep it out of source control:
# RUSTANGO_SESSION_SECRET=$(openssl rand -base64 32)
# RUSTANGO_SESSION_SECRET=
# Log filter, standard RUST_LOG syntax. Default is `info,sqlx=warn`.
# Production sets this in the orchestrator rather than here.
# RUST_LOG=info,my_app=debug
"
)
}
pub const GITIGNORE: &str = "/target
/.env
# Runtime state. The server writes generated session signing keys here
# when RUSTANGO_SESSION_SECRET is unset — never commit them.
/var/
# Local databases (sqlite backend, test fixtures).
*.db
*.db-wal
*.db-shm
*.db-journal
*.log
";
pub const RUST_TOOLCHAIN: &str = "[toolchain]
channel = \"stable\"
components = [\"rustfmt\", \"clippy\", \"rust-analyzer\"]
";
pub fn docker_compose(name: &str, backend: Backend) -> String {
let db_service = match backend {
Backend::Postgres => format!(
r#" postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: rustango
POSTGRES_PASSWORD: rustango
POSTGRES_DB: {name}_dev
# Without this the data lives in the container's writable layer, so
# `docker compose down` deletes your development database and `up`
# hands back an empty one, with nothing said either way (#1309).
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rustango -d {name}_dev"]
interval: 2s
timeout: 2s
retries: 20
"#
),
Backend::Mysql => format!(
r#" mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: rustango
MYSQL_DATABASE: {name}_dev
MYSQL_USER: rustango
MYSQL_PASSWORD: rustango
# Without this the data lives in the container's writable layer, so
# `docker compose down` deletes your development database and `up`
# hands back an empty one, with nothing said either way (#1309).
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u rustango -prustango"]
interval: 2s
timeout: 2s
retries: 30
"#
),
Backend::Sqlite => String::new(),
};
let db_volume = match backend {
Backend::Postgres => " postgres-data:\n",
Backend::Mysql => " mysql-data:\n",
Backend::Sqlite => "",
};
let (skip_hint, depends_on) = match backend.service() {
Some(svc) => (
format!(
" # Skip it and run cargo on the host with\n \
# `docker compose up -d --no-deps {svc}`."
),
format!(" depends_on:\n {svc}:\n condition: service_healthy\n"),
),
None => (
" # Skip it by running `cargo run` on the host — with SQLite\n \
# there is nothing else compose needs to start."
.to_owned(),
String::new(),
),
};
format!(
r#"services:
{db_service} # Hot-reload Rust dev container. `cargo watch -x run` rebuilds and
# restarts the binary on every source edit.
{skip_hint}
rust:
# Dockerfile.dev, not Dockerfile: the plain one is the deployable
# release image, which copies the source in and would defeat the
# bind mount below.
build:
context: .
dockerfile: Dockerfile.dev
{depends_on} env_file:
- .env
volumes:
- ./:/app
- cargo-target:/app/target
- cargo-registry:/usr/local/cargo/registry
- cargo-git:/usr/local/cargo/git
environment:
CARGO_TARGET_DIR: /app/target
CARGO_INCREMENTAL: "1"
command: bash -c "cargo watch -x run"
ports:
- "8080:8080"
volumes:
{db_volume} cargo-target:
cargo-registry:
cargo-git:
"#
)
}
pub fn dockerfile_dev() -> &'static str {
"FROM rust:1\n\
\n\
WORKDIR /app\n\
\n\
RUN cargo install cargo-watch\n"
}
pub fn dockerfile_prod(name: &str) -> String {
format!(
r#"# The deployable image. `Dockerfile.dev` is the hot-reload one
# docker-compose.yml uses; this one is for shipping.
#
# docker build -t {name}:latest .
# docker run --rm -p 8080:8080 -e DATABASE_URL=... {name}:latest
#
# Pick a non-default backend at build time:
# docker build --build-arg FEATURES=sqlite --build-arg NO_DEFAULT=1 .
FROM rust:1-bookworm AS builder
ARG FEATURES=""
ARG NO_DEFAULT=""
WORKDIR /src
COPY . .
# Incremental compilation only grows the cache in a one-shot image build.
ENV CARGO_INCREMENTAL=0
RUN set -eux; \
flags="--release --locked"; \
if [ -n "$NO_DEFAULT" ]; then flags="$flags --no-default-features"; fi; \
if [ -n "$FEATURES" ]; then flags="$flags --features $FEATURES"; fi; \
cargo build $flags; \
mkdir -p /out; \
cp target/release/{name} /out/
# Same Debian release as the builder: the binary links the builder's glibc.
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /out/{name} /usr/local/bin/{name}
# Migrations and settings are read at run time, so they travel with the
# binary rather than being baked into it.
COPY migrations /app/migrations
COPY config /app/config
# Never root. The numeric id keeps the ownership stable if the image is
# rebuilt on a host whose useradd picks a different one.
RUN useradd --uid 10001 --create-home app && chown -R 10001 /app
USER 10001
WORKDIR /app
EXPOSE 8080
# `.with_health()` mounts /health; drop this line if you removed it.
HEALTHCHECK --interval=5s --timeout=3s --start-period=20s --retries=12 \
CMD curl -fsS http://127.0.0.1:8080/health || exit 1
CMD ["{name}"]
"#
)
}
pub const DOCKERIGNORE: &str = "\
# Build output — the image builds its own, and this is the single
# biggest thing that would otherwise be uploaded to the daemon.
target/
**/target/
# Secrets. `.env.example` is the committed template and is safe.
.env
.env.*
!.env.example
# Local databases — a SQLite file here would be baked into the image.
*.db
*.db-wal
*.db-shm
*.db-journal
# Nothing in the image needs these.
.git/
.gitignore
node_modules/
**/node_modules/
";
pub fn readme(name: &str, template: Template, backend: Backend) -> String {
let template_label = match template {
Template::Api => "api (bare ORM + axum, no admin)",
Template::Fullstack => "fullstack (ORM + auto-admin)",
Template::Tenant => "tenant (multi-tenancy + operator console)",
};
let backend_label = backend.feature();
let (up_line, host_path) = match backend.service() {
Some(svc) => (
format!("docker compose up -d # boots {svc} + rust + cargo-watch"),
format!(
r#"### B. Cargo on the host (Docker just for {svc})
```sh
cp .env.example .env # then change `{svc}` -> `localhost` in DATABASE_URL
docker compose up -d {svc} # only the DB
cargo run -- migrate # apply pending migrations
cargo run # boot the HTTP server
cargo run -- --help # full verb list (makemigrations, startapp, etc.)
```"#
),
),
None => (
"docker compose up -d # boots rust + cargo-watch".to_owned(),
r#"### B. Cargo on the host (no Docker at all)
SQLite is a file next to the project, so nothing else has to be running.
```sh
cp .env.example .env
cargo run -- migrate # creates the database file and applies migrations
cargo run # boot the HTTP server
cargo run -- --help # full verb list (makemigrations, startapp, etc.)
```"#
.to_owned(),
),
};
format!(
r#"# {name}
Generated with `cargo rustango new {name}` — template `{template_label}`,
backend `{backend_label}`.
## Run locally — two paths
### A. All-in-Docker (default; hot-reload via cargo-watch)
```sh
cp .env.example .env
{up_line}
docker compose run --rm rust cargo run -- migrate
# server lives at http://localhost:8080 — edits to src/ trigger rebuild
```
The `rust` service runs `cargo watch -x run` against the bind-mounted
source tree. Three named volumes preserve incremental build state
across container restarts so a fresh `up` doesn't recompile from
scratch.
{host_path}
Either way: `cargo run` (no args) is `runserver`. Every other
management verb (`makemigrations`, `migrate`, `startapp`, `check`, …)
flows through the same binary via `rustango::manage::Cli` — see
`src/main.rs`.
## Project layout
```text
src/
main.rs — Cli::new().api(urls::api()).run() boots both server + verbs
models.rs — every #[derive(Model)] lives here
views.rs — request handlers
urls.rs — pub fn api() -> Router aggregator
migrations/ — JSON migration files (committed to git)
```
Adding a new model is one struct in `models.rs`; the auto-admin sees
it immediately. See <https://github.com/ujeenet/rustango> for the full
feature list.
"#
)
}
pub fn main_rs(template: Template, name: &str) -> String {
let body = match template {
Template::Api => MAIN_RS_API,
Template::Fullstack => MAIN_RS_FULLSTACK,
Template::Tenant => MAIN_RS_TENANT,
};
body.replace("{crate_name}", &crate_name(name))
}
pub fn crate_name(package_name: &str) -> String {
package_name.replace('-', "_")
}
pub fn lib_rs(name: &str) -> String {
let krate = crate_name(name);
format!(
"//! `{krate}` — the application, as a library.
//!
//! Everything lives here rather than in `src/main.rs` so that binaries
//! under `src/bin/` can reach it. A file in `src/bin/` is its own
//! crate: `crate::` there is that binary, not this project, so a
//! worker generated by `manage make:worker` could not otherwise see
//! your models or job types.
//!
//! From `src/main.rs` or any `src/bin/*.rs`:
//!
//! ```ignore
//! use {krate}::urls;
//! use {krate}::models::SomeModel;
//! ```
//!
//! `manage startapp <name>` adds its `pub mod <name>;` line here.
pub mod models;
pub mod urls;
pub mod views;
"
)
}
const MAIN_RS_API: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed.
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.
// The app's modules live in the library target (src/lib.rs) so that
// binaries under src/bin/ — a worker from `manage make:worker`, say —
// can reach the same models and jobs this one does. A `src/bin/*.rs`
// file is its own crate, so `crate::` there is NOT this project.
use {crate_name}::urls;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new()
.api(urls::api())
.with_welcome() // friendly `/` on first run; drop once you have a root handler
// NOTE: no .with_settings_from_env() here. It is gated on the
// config feature, and this template is deliberately bare: ORM +
// axum + the manage dispatcher. Adding config alone does not
// help either — the settings layers it switches on also need
// session, signals, access_log, allowed_hosts and body_limit,
// which together are batteries, which is the fullstack template.
//
// So the config/*.toml this scaffolder writes are inert in an api
// project. Pre-existing, and worth fixing properly — either stop
// writing them here, or give the template the features to read
// them — but not by making a minimal template heavy.
.run()
.await
}
";
const MAIN_RS_FULLSTACK: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed. The auto-admin is not wired up for
//! you: add an `admin_router(pool)` helper to `src/urls.rs` and nest
//! it under `/admin` (see the getting-started guide, Step 11).
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.
// See the note in the api template: the app's modules live in the
// library target so `src/bin/*.rs` binaries can reach them.
use {crate_name}::urls;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new()
.api(urls::api())
.with_welcome() // friendly `/` on first run; drop once you have a root handler
.with_health() // /health + /ready endpoints for load balancers
// Loads config/*.toml for the RUSTANGO_ENV tier (default `dev`),
// then RUSTANGO__* env overrides. Without it the files the
// scaffolder writes are inert.
.with_settings_from_env()
.run()
.await
}
";
const MAIN_RS_TENANT: &str = r##"//! Tenant project entrypoint — HTTP server serving both the operator
//! console and per-tenant apps via subdomain routing.
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.
// See the note in the api template: the app's modules live in the
// library target so `src/bin/*.rs` binaries can reach them.
use {crate_name}::urls;
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
rustango::manage::Cli::new()
.tenancy()
.api(urls::api())
.with_welcome() // friendly `/` on first run; drop once you have a root handler
.with_health() // /health + /ready hit the registry pool
.run()
.await
}
"##;
pub fn models_rs(template: Template) -> String {
let header = "//! Project models — every #[derive(Model)] lives here.
//!
//! Adding a struct here makes it admin-visible automatically: the
//! macro populates the `inventory` registry that
//! `rustango::admin::router(pool)` walks.
use rustango::sql::Auto;
use rustango::Model;
// `#[derive(Model)]` registers this struct at *runtime* through `inventory`,
// which rustc cannot see — so fields only the framework reads look dead to it.
// Delete this line once your own code reads them.
#[allow(dead_code)]
#[derive(Model, Debug, Clone)]
#[rustango(table = \"item\", display = \"name\")]
pub struct Item {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(max_length = 64)]
pub name: String,
pub active: bool,
}
";
if matches!(template, Template::Tenant) {
format!(
"{header}
// Tenancy registry models (Org, Operator, User) come along automatically
// with the `tenancy` feature — you don't need to redefine them here.
"
)
} else {
header.to_owned()
}
}
pub const VIEWS_RS: &str = "//! Project views — the request handlers this project mounts.
use axum::response::Html;
pub async fn index() -> Html<&'static str> {
Html(
\"<!doctype html>\\n\\
<title>rustango app</title>\\n\\
<h1>Hello from Rustango!</h1>\\n\\
<p>The auto-admin (if enabled) is at <a href=\\\"/admin\\\">/admin</a>.</p>\",
)
}
pub async fn healthz() -> &'static str {
\"ok\"
}
";
pub fn urls_rs(template: Template) -> String {
match template {
Template::Api => {
"//! Project URL routing (template: api — no admin).
//!
//! `Router::new()` is the auto-mount anchor — `manage startapp`
//! inserts `.merge(crate::<name>::urls::api())` lines here.
use axum::routing::get;
use axum::Router;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
Template::Fullstack => {
"//! Project URL routing (template: fullstack — ORM + admin).
//!
//! `Router::new()` in `api()` is the auto-mount anchor —
//! `manage startapp` inserts `.merge(crate::<name>::urls::api())`
//! lines here.
//!
//! The admin is NOT wired up for you. Add an `admin_router(pool)` helper
//! here and nest it under `/admin` from `main.rs` — getting-started
//! Step 11 spells it out. Take `rustango::sql::Pool` so the helper names
//! no driver and the project still builds on all three backends.
use axum::routing::get;
use axum::Router;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
Template::Tenant => {
"//! Project URL routing (template: tenant).
//!
//! `Builder::api(...)` mounts these routes on every tenant
//! subdomain alongside the auto-admin. Handlers can take
//! `rustango::extractors::Tenant` to resolve the current tenant +
//! get a tenant-scoped `&mut PgConnection`. Example:
//!
//! ```ignore
//! pub async fn list_items(mut t: rustango::extractors::Tenant)
//! -> Result<axum::Json<Vec<crate::models::Item>>, axum::http::StatusCode> {
//! let rows = crate::models::Item::objects()
//! .fetch_on(t.conn()).await
//! .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
//! Ok(axum::Json(rows))
//! }
//! ```
use axum::routing::get;
use axum::Router;
use crate::views;
pub fn api() -> Router<()> {
Router::new()
.route(\"/\", get(views::index))
.route(\"/healthz\", get(views::healthz))
}
"
.to_owned()
}
}
}
pub fn config_default_toml(name: &str, backend: Backend) -> String {
let example_url = backend.host_url(name);
format!(
r##"# {name} — shared defaults across every tier
# (`config/default.toml` is loaded first; `config/<RUSTANGO_ENV>_settings.toml`
# overrides on top.) Every section is optional — uncomment + edit
# what you need.
# [database]
# url = "{example_url}"
# pool_min_size = 2
# pool_max_size = 20
# [admin]
# allowed_tables = [] # empty = every registered model
# read_only_tables = []
# [server]
# bind = "127.0.0.1:8080"
# request_timeout_secs = 30
# max_body_bytes = 2097152 # 2 MiB
# [auth]
# argon2_memory_kib = 19456 # OWASP 2024 floor
# argon2_iterations = 2
# lockout_threshold = 5
# lockout_duration_secs = 900
# [auth.jwt]
# access_ttl_secs = 900 # 15 min
# refresh_ttl_secs = 604800 # 7 days
# issuer = "{name}"
# [brand]
# name = "{name}"
# tagline = ""
# primary_color = "#2c6fb0"
# theme_mode = "auto" # auto | light | dark
# [security]
# headers_preset = "strict"
# hsts_max_age_secs = 31536000 # 1 year
# cors_allowed_origins = []
# [routes]
# legacy_preset = false
# # Per-field overrides: login_url / admin_url / audit_url / static_url /
# # brand_url / change_password_url / impersonation_handoff_url
# [audit]
# retention_days = 90
# [logging] # needs `Cli::with_logging()` AND
# # `#[rustango::main(logging = false)]` —
# # see config/prod_settings.toml for why
# level = "info,sqlx=warn" # RUST_LOG syntax; RUST_LOG still wins
# format = "pretty" # pretty | json | compact
# with_line_numbers = false
# file_dir = "/var/log/{name}" # set to also write a rolling file
# file_rotation = "daily" # daily | hourly | minutely | never
# [mcp] # Model Context Protocol server (feature = "mcp")
# prefix = "/mcp" # URL prefix the MCP router mounts under
# token_ttl_secs = 900 # agent access-token lifetime (15 min)
# enable_sse = true # serve the GET {{prefix}} SSE stream
# allowed_origins = [] # CORS allow-list (empty = same-origin only)
# rate_limit_per_minute = 0 # per-IP cap (0/unset = unlimited)
# max_tools_listed = 0 # tools/list page size (0/unset = unlimited)
"##
)
}
pub fn config_dev_settings_toml(name: &str, backend: Backend) -> String {
let url = backend.host_url(name);
format!(
r##"# {name} — local development tier
# Loaded when RUSTANGO_ENV=dev (the default when unset).
[database]
# Matches docker-compose.yml and .env.example. Host is `localhost`, not
# the compose service name: this tier runs the app on the host against
# the container.
url = "{url}"
[server]
bind = "127.0.0.1:8080"
[security]
# Drop strict headers in dev so http<->https rebinds don't lock the
# browser into HSTS.
headers_preset = "dev"
hsts_max_age_secs = 0
[brand]
# Make the dev tier visually distinguishable from prod.
tagline = "(dev)"
"##
)
}
pub fn config_staging_settings_toml(name: &str, backend: Backend) -> String {
let staging_url = match backend {
Backend::Postgres => format!("postgres://staging-host/{name}_staging"),
Backend::Mysql => format!("mysql://staging-host/{name}_staging"),
Backend::Sqlite => format!("sqlite://./{name}_staging.db?mode=rwc"),
};
format!(
r##"# {name} — staging tier
# Loaded when RUSTANGO_ENV=staging. Production-shape security
# headers, but pointed at a separate database with shorter retention
# so QA volume doesn't bleed into prod analytics.
# [database]
# url = "{staging_url}"
[server]
bind = "0.0.0.0:8080"
[security]
headers_preset = "strict"
hsts_max_age_secs = 31536000
[brand]
tagline = "(staging)"
[audit]
retention_days = 30
"##
)
}
pub fn config_prod_settings_toml(name: &str) -> String {
format!(
r##"# {name} — production tier
# Loaded when RUSTANGO_ENV=prod. Strict-by-default; sensitive values
# (database url, secret key) come from the environment or your secrets
# manager — leaving them out of source control.
# The URL comes from the `DATABASE_URL` environment variable, which is
# what every pool reads. Set it from your secrets manager.
#
# Pool sizes can be pinned here. If you do, uncomment the header *with*
# the keys: a key left uncommented under a commented-out `[database]`
# lands at the TOML root, where it is silently ignored.
# [database]
# pool_min_size = 5
# pool_max_size = 50
[server]
bind = "0.0.0.0:8080"
request_timeout_secs = 30
[security]
headers_preset = "strict"
hsts_max_age_secs = 31536000
[audit]
retention_days = 365
# Read by `Cli::with_logging()`, which this project does NOT call. To make
# this section live: add `.with_logging()`, and change `src/main.rs` to
# `#[rustango::main(logging = false)]` so the macro's own subscriber steps
# aside — `tracing` keeps the first one installed, so without that the
# settings here are read and discarded (#1465).
# `RUST_LOG` overrides `level` either way, and works today.
# [logging]
# level = "info"
# format = "json" # one object per event, for a log aggregator
"##
)
}