Configuration#

Bialet stores configuration in the SQLite database — every setting is a row in a simple key/value table. There are no .env files, no YAML, no environment variables to set.

Reading and Writing#

Config.set("app_name", "My App")
var name = Config.get("app_name")  // "My App"

Typed Access#

All values are stored as strings. Use typed getters for numbers, booleans, and JSON:

Config.set("items_per_page", "20")
var limit = Config.num("items_per_page")  // 20 as a number

Config.set("maintenance_mode", "1")
var on = Config.bool("maintenance_mode")  // true

Config.json("features", {"darkMode": true, "notifications": false})
var features = Config.json("features")     // { "darkMode": true, ... }

Config.bool returns true for "1" and "true" (case-insensitive), false for everything else including missing keys.

Enable / Disable#

For on/off flags, use the enable and disable convenience methods instead of hand-writing the "1"/"0" strings:

Config.enable("beta_features")   // stores "1"
Config.disable("beta_features")  // stores "0"
Config.bool("beta_features")     // false

if (Config.bool("live_reload")) {
  // feature is on
}

enable sets the value to "1", disable sets it to "0". Both are equivalent to Config.set("key", "1") and Config.set("key", "0").

Delete#

Config.delete("old_setting")

⚠️ Pitfall: Config.get() returns null for missing keys. Always provide a fallback: Config.get("title") || "Default Title".

How It Works#

Under the hood, Config reads and writes to the BIALET_CONFIG table, a two-column table created automatically when the database initializes:

CREATE TABLE IF NOT EXISTS BIALET_CONFIG (
  key   TEXT PRIMARY KEY,
  value TEXT
)

Config.set("key", "value") runs an INSERT OR REPLACE. Config.get("key") does a SELECT value FROM BIALET_CONFIG WHERE key = ?. .num, .bool, and .json convert the string result into the right type in Wren — the database still stores everything as TEXT.

Because configuration is just rows in SQLite, changes persist across server restarts with no extra work. There’s no file to parse, no cache to invalidate, and no separate config server to run.

Seeding Configuration#

With -r at startup#

The -r flag runs Wren code before the server starts and then exits. Use it in deployment scripts to seed secrets and settings:

bialet -r 'Config.set("stripe_key", "sk_live_...")' /www/myapp
bialet -r 'Config.set("mail_host", "smtp.example.com")' /www/myapp

-r can run any Wren code, so you can set multiple values in one call or even build a setup script:

// setup.wren — run with: bialet -r "$(cat setup.wren)" /www/myapp
Config.set("title", "My App")
Config.set("items_per_page", "20")
Config.json("features", {"darkMode": true, "notifications": false})

Use -r for one-off provisioning — secrets, API keys, and environment-specific overrides that shouldn’t live in migrations.

With migrations for defaults#

For defaults that ship with the application and should exist in every environment, write them in a migration:

// _migration.wren
Db.migrate("Add default config", [
  `INSERT OR IGNORE INTO BIALET_CONFIG VALUES ('title', 'My App')`,
  `INSERT OR IGNORE INTO BIALET_CONFIG VALUES ('items_per_page', '20')`,
  `INSERT OR IGNORE INTO BIALET_CONFIG VALUES ('admin_email', 'admin@example.com')`
])

INSERT OR IGNORE sets a fallback value — it only inserts if the key doesn’t already exist. This lets -r overrides from deployment scripts take precedence over migration defaults.

Working with Environments#

Bialet has no development, staging, or production mode. Each environment is a different database file with its own BIALET_CONFIG table. The database is the environment.

dev/
├── _db.sqlite3          # Dev config
├── _migration.wren
└── index.wren

prod/
├── _db.sqlite3          # Prod config
├── _migration.wren
└── index.wren

Migrations and application code are identical across environments — only the database file differs, and therefore only the configuration and data differ. Pointing Bialet at a different _db.sqlite3 switches environments.

Comparison with Other Frameworks#

Most frameworks layer configuration from multiple sources: .env files, environment variables, YAML configs, command-line flags, and sometimes a configuration database. They merge them with precedence rules (env vars beat .env, flags beat env vars, etc.), and you need to know which source wins in which context.

Bialet removes that entire stack. There is one source: the database.

Bialet

Rails / Django / Laravel

Express / Next.js

Config storage

SQLite rows

.env + YAML/rb/py + ENV

.env + ENV

Precedence rules

None

Multi-layer (env beats file, etc.)

Multi-layer

Persists across restarts

Yes (it’s a DB row)

No (env vars are per-process)

No

Programmatic write

Config.set()

Usually read-only from code

Usually read-only

Environment switching

Point to a different _db.sqlite3

RAILS_ENV, NODE_ENV, etc.

NODE_ENV

Configuration UI

Possible — just writes to SQLite

Needs a backing store

Needs a backing store

Secrets at deploy time

bialet -r 'Config.set(...)'

.env file or CI env vars

.env file or CI env vars

The trade-off: you can’t configure Bialet through environment variables or .env files. If your deployment pipeline relies on those, you’ll need to translate them into -r calls or migration defaults. The benefit: no precedence bugs, no runtime parsing overhead, and a single table to query for every setting in the system.

Common Patterns#

Feature Flags#

if (Config.bool("beta_features")) {
  // Show beta UI
}

Toggle flags with Config.enable() / Config.disable():

Config.enable("beta_features")   // turn on
Config.disable("beta_features")  // turn off

Maintenance Mode#

if (Config.bool("maintenance_mode")) {
  Response.status(503)
  return <main><h1>We'll be right back</h1></main>
}

Pagination Settings#

var limit = Config.num("items_per_page") || 20
var page = Num.fromString(Request.get("page") || "1")
var offset = (page - 1) * limit

Site-Wide Title#

var title = Config.get("site_title") || "My Site"

Pitfalls#

  • Config.get() returns null for missing keys — always provide a fallback: Config.get("key") || "default".

  • Config values are strings — use Config.num() and Config.bool() for typed access. Config.get("port") returns "7001", not 7001.

  • No .env files or environment variables — everything lives in BIALET_CONFIG. If you need inject values from outside, use -r.

  • -r runs once and exits — it’s for provisioning, not for setting values that the server reads on every request. Those belong in migrations or are set once via -r and then read via Config.get() at runtime.

  • Keep your production database out of version control — it contains your config, user data, and sessions.

Next Steps#

  • Learn about Database — the Query object, migrations, and how BIALET_CONFIG sits alongside your application tables

  • Set up a production environment with Deployment

  • See the Security guide for CSRF, headers, and secure configuration

  • Check the Reference for the full Config API