Appearance
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:
- Database — an admin override saved through the panel (
app_settingstable). - Environment — the
.envvalue the config file reads. - 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:cachenote. Hydration is a no-op whilephp artisan config:cache/optimizebuild the cached config, so a database override (or a decrypted secret) is never written tobootstrap/cache/config.php. Overrides are re-applied per worker at runtime instead. You may safely runconfig:cachein 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 viaPlatformSettings::get()and never entersconfig()at all.
Sections & settings
Column key: Setting (panel key) · Config path (what it overrides) · Type · notes.
General & branding (general)
| Setting | Config path | Type |
|---|---|---|
| general.app_name | app.name | string |
| general.support_email | imaginook.branding.support_email | |
| general.attribution | imaginook.branding.attribution | bool |
| general.maintenance | imaginook.branding.maintenance | bool |
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)
| Setting | Config path | Type |
|---|---|---|
| mail.mailer | mail.default | select |
| mail.smtp_host / _port / _username | mail.mailers.smtp.* | text / int / text |
| mail.smtp_password | mail.mailers.smtp.password | secret |
| mail.smtp_scheme | mail.mailers.smtp.scheme | select |
| mail.from_address / _name | mail.from.* | email / text |
| mail.mailgun_domain / _endpoint | services.mailgun.{domain,endpoint} | text / select |
| mail.mailgun_secret | services.mailgun.secret | secret |
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)
| Setting | Config path | Type |
|---|---|---|
| domains.subdomain_tlds | imaginook.subdomain_tlds | list |
| domains.reserved_names | imaginook.reserved_names | list (additive to built-ins) |
| domains.profanity | imaginook.profanity | list (empty = off) |
| domains.rename_cooldown_days | imaginook.freed_cooldown_days | int |
| domains.host_map_auto_reload | imaginook.provisioning.auto_reload | bool |
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)
| Setting | Config path | Type |
|---|---|---|
| access.registration_mode | imaginook.signup.registration_mode | select (open/invite/closed) |
| access.captcha_enabled | imaginook.signup.captcha_enabled | bool |
| access.age_floor_default / _eu | legal.age.{default_floor,eu_floor} | int (≥ 13) |
| access.signup_cooldown_hours | abuse.signup_cooldown_hours | int |
| access.disposable_email_domains | abuse.disposable_email_domains | list |
| access.login_per_minute / auth_per_minute | imaginook.access.* | int |
An unrecognised registration_mode fails safe to invite-only. The age floor can never go below 13 (COPPA).
Quotas & limits (quotas)
| Setting | Config path | Type |
|---|---|---|
| quotas.default_storage_mb | imaginook.quotas.default_quota_bytes | int (MB) |
| quotas.default_bandwidth_gb | imaginook.quotas.default_bandwidth_bytes | int (GB) |
| quotas.max_upload_mb | imaginook.uploads.max_file_bytes | int (MB) |
| quotas.max_zip_mb | imaginook.zip.max_upload_bytes | int (MB) |
| quotas.trash_retention_days / trash_max_entries | imaginook.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)
| Setting | Config path | Type |
|---|---|---|
| safety.fail_closed | imaginook.scanning.fail_closed | bool |
| safety.clamav_socket / _timeout / _max_stream_mb | imaginook.scanning.clamav.* | text / int / int (MB) |
| safety.safe_browsing_api_key | imaginook.scanning.safe_browsing.api_key | secret |
| safety.urlhaus_enabled / _endpoint | imaginook.scanning.urlhaus.* | bool / url |
| safety.csam_driver | imaginook.csam.driver | select |
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)
| Setting | Config path | Type |
|---|---|---|
| notifications.publish_cooldown_hours | imaginook.notifications.publish_cooldown_hours | int |
| notifications.webhook_enabled | imaginook.notifications.webhook.enabled | bool |
| notifications.webhook_url | imaginook.notifications.webhook.url | url |
| notifications.webhook_secret | imaginook.notifications.webhook.secret | secret |
| 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)
| Setting | Config path | Type |
|---|---|---|
| federation.instance_name | imaginook.federation.instance_name | string |
| federation.feed_limit | imaginook.federation.feed_limit | int |
| federation.peers | imaginook.federation.peers | list 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_modefails 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/).