Skip to content

Configuration manager

An admin-only control panel (/admin/config, behind the staff + mandatory-2FA + can:admin gate) for tuning platform settings without editing .env or redeploying. It is designed so a 12-factor / Docker-env deployment keeps working untouched: every setting has an environment default, and the panel only ever overrides it.

How a setting resolves

Each setting resolves in this order, highest priority first:

  1. Database — an admin override saved through the panel (app_settings table).
  2. Environment — the .env value the config file reads.
  3. Default — the hardcoded fallback in the config file.

At boot, PlatformSettings::hydrate() applies the database overrides onto Laravel's config() so the rest of the app keeps reading plain config(). An instance that never opens the panel behaves exactly as its .env/defaults dictate.

config:cache note. Hydration is a no-op while php artisan config:cache / optimize build the cached config, so a database override (or a decrypted secret) is never written to bootstrap/cache/config.php. Overrides are re-applied per worker at runtime instead. You may safely run config:cache in production.

Secrets

Settings marked secret (passwords, API keys, signing secrets) are:

  • encrypted at rest (Crypt), and stored as ciphertext even in the settings cache;
  • write-only — the panel never sends a secret's value back; it shows only a "configured / not set" placeholder, and an empty submission leaves it unchanged;
  • redacted everywhere — never in an API response, the audit log, or an export;
  • framework- vs app-consumed. A framework-consumed secret (the mail password) is applied to config() at runtime so Laravel's mailer can read it; an app-consumed secret (the webhook signing secret) is read lazily via PlatformSettings::get() and never enters config() at all.

Sections & settings

Column key: Setting (panel key) · Config path (what it overrides) · Type · notes.

General & branding (general)

SettingConfig pathType
general.app_nameapp.namestring
general.support_emailimaginook.branding.support_emailemail
general.attributionimaginook.branding.attributionbool
general.maintenanceimaginook.branding.maintenancebool

Maintenance mode shows a 503 to non-staff; staff keep access so they can turn it back off (the EnsureNotInMaintenance middleware reads the toggle live and is never applied to the login/admin routes).

Mail (mail)

SettingConfig pathType
mail.mailermail.defaultselect
mail.smtp_host / _port / _usernamemail.mailers.smtp.*text / int / text
mail.smtp_passwordmail.mailers.smtp.passwordsecret
mail.smtp_schememail.mailers.smtp.schemeselect
mail.from_address / _namemail.from.*email / text
mail.mailgun_domain / _endpointservices.mailgun.{domain,endpoint}text / select
mail.mailgun_secretservices.mailgun.secretsecret

Send test email delivers a fixed message to your own admin address using the saved settings; failures are shown with any secret scrubbed out.

Domains & subdomains (domains)

SettingConfig pathType
domains.subdomain_tldsimaginook.subdomain_tldslist
domains.reserved_namesimaginook.reserved_nameslist (additive to built-ins)
domains.profanityimaginook.profanitylist (empty = off)
domains.rename_cooldown_daysimaginook.freed_cooldown_daysint
domains.host_map_auto_reloadimaginook.provisioning.auto_reloadbool

The nginx reload command itself is env-only (NGINX_RELOAD_COMMAND) and is deliberately not editable here — it is executed, so a web-editable command would be remote code execution. The toggle only gates whether it fires.

Signup & access (access)

SettingConfig pathType
access.registration_modeimaginook.signup.registration_modeselect (open/invite/closed)
access.captcha_enabledimaginook.signup.captcha_enabledbool
access.age_floor_default / _eulegal.age.{default_floor,eu_floor}int (≥ 13)
access.signup_cooldown_hoursabuse.signup_cooldown_hoursint
access.disposable_email_domainsabuse.disposable_email_domainslist
access.login_per_minute / auth_per_minuteimaginook.access.*int

An unrecognised registration_mode fails safe to invite-only. The age floor can never go below 13 (COPPA).

Quotas & limits (quotas)

SettingConfig pathType
quotas.default_storage_mbimaginook.quotas.default_quota_bytesint (MB)
quotas.default_bandwidth_gbimaginook.quotas.default_bandwidth_bytesint (GB)
quotas.max_upload_mbimaginook.uploads.max_file_bytesint (MB)
quotas.max_zip_mbimaginook.zip.max_upload_bytesint (MB)
quotas.trash_retention_days / trash_max_entriesimaginook.trash.*int

The MB/GB settings are entered in their natural unit — the panel scales them to bytes for the config. Storage/bandwidth defaults apply to new accounts; existing per-account overrides are untouched.

Scanning & safety (safety)

SettingConfig pathType
safety.fail_closedimaginook.scanning.fail_closedbool
safety.clamav_socket / _timeout / _max_stream_mbimaginook.scanning.clamav.*text / int / int (MB)
safety.safe_browsing_api_keyimaginook.scanning.safe_browsing.api_keysecret
safety.urlhaus_enabled / _endpointimaginook.scanning.urlhaus.*bool / url
safety.csam_driverimaginook.csam.driverselect

The section also surfaces the moderation strictness preset (baseline / strict / minimal — applied through the shared moderation-policy engine) and links to the full moderation-policy page and the safety-feed allow/block lists. The illegal-content floor (a detected CSAM/malware hit) is independent and cannot be relaxed by any of these settings.

Notifications (notifications)

SettingConfig pathType
notifications.publish_cooldown_hoursimaginook.notifications.publish_cooldown_hoursint
notifications.webhook_enabledimaginook.notifications.webhook.enabledbool
notifications.webhook_urlimaginook.notifications.webhook.urlurl
notifications.webhook_secretimaginook.notifications.webhook.secretsecret
notifications.webhook_imaginook.notifications.webhook.events.*bool

The outbound webhook POSTs the enabled events to your URL, each HMAC-SHA256 signed (X-Imaginook-Signature: t=<ts>,v1=<hex> over "<timestamp>.<body>") so your receiver can verify authenticity within a timestamp tolerance. Send test webhook delivers a signed sample so you can confirm your endpoint before enabling it.

Federation (federation)

SettingConfig pathType
federation.instance_nameimaginook.federation.instance_namestring
federation.feed_limitimaginook.federation.feed_limitint
federation.peersimaginook.federation.peerslist of URLs

The section also toggles the federated safety-feed consume/contribute (a persisted setting read live by the publisher + sync command) and links to Bridgy Fed setup for fediverse follow/reply.

Advanced (advanced)

No editable settings — hosts three tools:

  • Export downloads your non-default settings as JSON (secrets are never included), for backup or moving between instances.
  • Import re-applies a file; every value is validated against the registry (all-or-nothing), and unknown or secret keys are skipped — a secret can never be set through import.
  • Effective configuration — a read-only table of every setting resolved DB → env → default, with its source, secrets shown only as a set/unset mask.
  • Diagnostics — a non-sensitive health panel (env, PHP version, DB-override count, cache/queue drivers, mail mailer, webhook/maintenance flags).

Guarantees

  • Admin-only. Every route requires staff + 2FA + can:admin.
  • Can't brick. No setting combination locks an admin out (maintenance keeps staff in; a bad registration_mode fails safe to invite; the age floor can't drop below 13; a degenerate profanity entry can't reject every label).
  • No RCE. The registry is an allowlist — a submission for any key it doesn't know is rejected, and no setting maps to an executed path (the nginx reload command stays env-only) or a privileged key (app.key, database.*).
  • Secrets stay secret. Encrypted at rest, write-only, and never present in the cache, an audit entry, a response, or an export.

Every one of these is asserted by the test suite (tests/Feature/Config/).

AGPL-3.0 · built in the open.