Tutorial 09: Environment Management & Configuration ⚙️
Rullst provides a validated runtime environment shared by its subsystems. New
projects use RULLST_ENV; APP_ENV remains a legacy compatibility alias.
🛠️ Step 1: Define Environment Variables in .env
cargo rullst new already writes .env with RULLST_ENV=development, a random
APP_KEY and the starter’s DATABASE_URL, plus a .env.example with a
placeholder key. A hand-written project adds the file itself:
RULLST_ENV=development
PORT=3000
APP_KEY=REPLACE_WITH_YOUR_32_CHAR_RANDOM_KEY
DATABASE_URL=postgres://postgres:password@localhost:5432/my_app
Replace the placeholder with at least 32 bytes of random data, for example the
output of openssl rand -base64 32: rullst-auth, which encrypts session
cookies with APP_KEY, refuses shorter keys and the documented placeholders.
PORT overrides the port passed to Server::run.
💻 Step 2: Access Configuration in Rust
#![allow(unused)]
fn main() {
use rullst::config::RullstConfig;
fn report_environment() -> Result<(), Box<dyn std::error::Error>> {
let environment = RullstConfig::global().environment()?;
// The process environment first, then `./.env`, as `Server` reads it.
let database_is_configured =
rullst::config::project_setting("DATABASE_URL")?.is_some();
println!(
"Booting Rullst in {environment} mode; database configured: {database_is_configured}"
);
Ok(())
}
}
Do not log DATABASE_URL: it commonly embeds a username and password. Report
only whether configuration is present, and redact sensitive fields in structured
telemetry.
The generated server bootstrap reads .env before applying its runtime policy.
Neither Server nor the Artisan commands (db:migrate, db:seed, studio)
load .env into the process environment, so std::env::var sees only real
process variables there; read application settings with
rullst::config::project_setting, which checks the process environment and
then ./.env, and behaves the same in the server and in a seeder. Standalone
utilities must load a dotenv file themselves or receive exported
process variables. Environment precedence is exact: the process RULLST_ENV,
legacy APP_ENV, then RULLST_ENV or APP_ENV in .env, then [app].env in
Rullst.toml. Server never exports .env into the process environment;
RullstConfig::environment() includes the .env step once a Server has
started in the process, so it reports the environment the server enforces.
Before that, and in standalone utilities, it reads only the process variables
and [app].env, as Environment::detect always does. Unknown values are configuration
errors rather than silently becoming development. rullst-auth cookie helpers
also consult .env below the process variables and skip reading it when either
is set; a malformed .env yields a fixed error that never quotes file content.
A malformed or mistyped Rullst.toml fails with only its line and column, so
the error can be logged without exposing app_key or connection strings.
Step 3: Configure the Browser Security Baseline
Server applies the same public apply_security_baseline composition in
staging and production. CORS is deny-by-omission: list exact origins without a
trailing slash, and enable credentialed cross-origin requests only when the
application genuinely needs them.
[security]
csrf_same_site = "Strict"
cors_allow_origins = ["https://academy.example"]
cors_allow_credentials = false
csp = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'nonce-{NONCE}'; style-src 'self' 'nonce-{NONCE}'"
coep = "require-corp"
Wildcard, path-bearing, credential-bearing, queried or duplicate CORS origins
are configuration errors, as are spellings a browser never sends: an uppercase
scheme or host, or an explicit default port (:443 for HTTPS, :80 for HTTP).
CORS compares the configured origin with the Origin header byte for byte. When credentials are enabled, Core still grants them
only to an origin in the exact allowlist. Test the final policy behind the real
TLS proxy because an intermediary can change headers and cookie behavior.
coep accepts only require-corp, credentialless, or unsafe-none.
Keep the strict require-corp default for same-origin assets. If an application
must load reviewed cross-origin media whose server does not emit CORP, it can
explicitly select credentialless and update the relevant CSP source directive;
the browser then omits credentials from those cross-origin no-CORS requests.
unsafe-none lowers cross-origin isolation and should be chosen only after an
application threat-model review. COEP configuration never makes an arbitrary
remote origin trustworthy.
The same baseline adds Cache-Control: no-store to every response that has no
Cache-Control header of its own, so pages with personal data are not stored
by browsers or shared caches. Responses that should be cached, such as file
downloads or calendar and RSS feeds, must set their own header; the standard
/static mount already does.
💡 Key Takeaways
- Never hardcode credentials, database passwords, or JWT secrets in
.rssource code. - Add
.envto.gitignoreand commit.env.exampleto document required keys for team members. - Prefer
RULLST_ENV; keepAPP_ENVonly while migrating an existing project.