VoltexaHubDocumentation1.8.0Install the beta
Browse documentation

Reference

Operations reference

Complete recovery, upgrades and optional official-site operations.

Includes the complete docs/OPERATIONS.md reference from this build. Application paths refer to your own installation.

On this page

Backups, restore and upgrades#

VoltexaHub's recoverable state is the MySQL database, private uploads and original APP_KEY together. Attachments, avatars and branding live on the uploads disk; account credentials and references live in the database. A backup without any one of these is incomplete. Store recovery bundles in a private location outside public/, with access limited to operators. Bundles contain private member data and the plaintext application key; never commit, share publicly or place one under the web root. Test recovery regularly with synthetic or approved data.

Create a consistent bundle#

Use a mysqldump client matching the supported MySQL server version and the matching application .env. Put the site in maintenance mode (php artisan down), stop queue workers and other writers, and ensure no write can occur until the dump and upload copy finish. Then run:

php scripts/recovery.php backup /private/backups/forum-YYYYMMDD --acknowledge-quiesced
php artisan up

The output directory must be new. The script writes database.sql, uploads/, app-key.txt, and a manifest listing SHA-256 for each file. Checksums detect accidental corruption; they do not authenticate an untrusted SQL bundle. Restore only an operator-trusted bundle. The script uses a temporary private MySQL option file and never prints credentials or key material. Set MYSQLDUMP_BIN to the full executable path of a client matching the server version if mysqldump is not on PATH. On Windows, verify that the destination inherits restrictive ACLs; Unix mode bits alone do not secure a Windows backup. --acknowledge-quiesced records the operator's write freeze; the script cannot stop external writers itself. Treat a failed or interrupted bundle as invalid and start with a new output directory. The bundle is a point-in-time snapshot only when writes are frozen across both database and uploads.

Restore into isolation#

Create a new empty supported MySQL database on loopback and a separate installation. Configure that installation's DB_* variables to the new database. The restore command refuses a nonempty database, an existing uploads path or key output, a destination inside public/, a nonlocal target, the recorded source database, symlinks, path traversal, unexpected files and checksum mismatches. It validates the complete bundle before writing:

php scripts/recovery.php restore /private/backups/forum-YYYYMMDD \
  --uploads-target=/private/restore/uploads \
  --key-output=/private/restore/recovered-key.env \
  --acknowledge-isolated

Set MYSQL_BIN to the full mysql client path if needed. The key output contains APP_KEY=...; copy that value into the isolated installation's private .env without printing or regenerating it. Set UPLOADS_ROOT to the restored uploads path. Bring up the application with the matching code release and check login, post attachments, member avatars and custom branding. Check that private files remain permission-gated. Never serve the original database or upload path during a drill.

MySQL DDL is not transactional. If SQL import or later file copy fails, the destination may be partial. The script intentionally leaves it for diagnosis and refuses a retry into that database; recreate a fresh empty destination database and new uploads/key paths before retrying. Do not delete the only source bundle. The restore tool is limited to local, isolated MySQL targets; remote production recovery requires an operator-controlled equivalent process and a tested maintenance window.

Upgrade and rollback#

Before an upgrade, freeze writes and create a full bundle. Keep the previous code release and bundle together. Extract the new release into a different directory, retain the existing private .env values (especially APP_KEY), database and upload volume, install Composer production dependencies, run php artisan migrate --force and php artisan forum:plugins:upgrade-bundled, clear stale configuration/views with php artisan optimize:clear, restart queue workers, and resume traffic only after application and file checks pass. Never run migrate:fresh or roll back migrations on a live community. Plugin migrations can have nontransactional DDL; follow the plugin lifecycle guidance in extension development. Review third-party extensions separately; the bundled-plugin command preserves installed/enabled state and does not upgrade external plugins. See release gates for the automated prior-release rehearsal.

If the upgrade fails, stop writes. Do not point old code at a newly migrated database. Restore the matching pre-upgrade bundle into a new empty database and uploads directory, use the previous code release and original APP_KEY, check accounts and files, then switch traffic to that known-good combination. Any writes accepted after the backup are absent from this rollback and must be accounted for separately. Preserve failed state for diagnosis until a recovery is verified.

Healthcheck and public media caching#

/up returns 200 OK only when the database answers a trivial query, and 503 otherwise, so a platform healthcheck fails during a database outage instead of passing against a web process that cannot serve pages. It is unauthenticated, starts no session and reveals only up or down; the failure is logged with the exception class and no message.

Member avatars (/members/{id}/avatar), branding images (/branding/{slot}) and declared theme CSS/JS (/theme-assets/{theme}/{file}) are public, so they are served without a session, cookie, theme or plugin boot or mail refresh. Anonymous requests create no session row. A URL carrying the current v= token, which the application adds to every link it renders, is sent with Cache-Control: public, max-age=31536000, immutable. A new avatar or branding upload gets a new stored path and a theme asset a new content hash, so the token changes and the old URL is never reused for new bytes. A request with no token, or a stale one, always receives the current file with Cache-Control: public, no-cache and an ETag, so it revalidates and cannot pin outdated content. Avatars are not hidden from banned viewers, because a cacheable response cannot depend on who is asking. A CDN or reverse proxy in front of the application can now cache these paths safely. Purge or expire an avatar at the cache layer if an image must be withdrawn before its cached copies expire, since a removed or replaced avatar's old URL stays valid in caches that already hold it.

Administrator email overrides#

Web requests and the queue JobProcessing event refresh encrypted email configuration and discard cached mailers when its fingerprint changes. Long-running workers pick up a changed override before their next job; inherited deployment configuration is captured when the process starts, so deployment environment changes still follow normal worker restart practices. Installation/migration console bootstrap performs no integration-table query.

Use the Email delivery page to distinguish provider submission from inbox receipt. Check sender verification, credentials, TLS and connectivity if a test fails; protected transports return safe guidance without retaining provider exception details or credentials. An unreadable override fails closed and can be recovered by explicit replacement or inheritance restoration. Delivery tests affect neither verification requirements nor member notification opt-in.

Marketing activity and private request lifecycle#

Private access requests contain the requesting account ID, community information, status, staff response and administrative notes. Owners never receive internal notes, including JSON responses. Request data has no automatic 90-day deletion: owners or authorized administrators may explicitly delete it; account deletion cascades its request. Reviews and deletions audit request IDs/status only, without request text, addresses or notes.

Anonymous measurement stores only UTC day, allowlisted page/event and a bounded activity count. These are activities, not unique visitors or conversion rates. No raw URL, query, referrer, IP, user agent or account ID is stored in aggregates. IP-based rate control uses a keyed hash in the normal cache for a 60-second window. Infrastructure access logs are separate from this application data policy; operators should review their retention independently.

DNT:1 and Sec-GPC:1 suppress browser measurement. Client opt-out prevents client submissions. The server independently counts successful new private-request submissions as an aggregate operational statistic; those counts are not suppressed by browser preferences. Editing a request does not count another submission. Deleting a request does not rewrite activity history.

Valid ingestion prunes aggregate dates older than the inclusive 90-day UTC window. Run php artisan marketing:prune for a deliberate prune without waiting for ingestion. The worker schedules this command daily at 03:15 UTC; it never deletes access requests. The administrator activity page presents the last 30 UTC days; protected queue reads/counts remain administrator-only. Counters saturate at 2,147,483,647 per daily page/event to avoid integer overflow.

Launch templates and attestations (1.8)#

Setup -> Starter templates previews three additive structures. Applying a confirmed preview creates new forums, a dedicated group and scoped rules; it never reorganizes existing content or global permissions. A template can be applied once. Optional welcome discussions are authored by the applying verified administrator. No users, payment plans or paid entitlements are created. Review the permission inspector before assigning members. The creator workshop denies current groups at its boundary and grants the new supporter group and Administrators access to its child forum; groups added later require another access review.

The shared launch checklist records explicit administrator attestations and timestamps. Reading Setup does not complete a task. Configuration evidence, provider acceptance and inbox receipt remain separate. Reopen a task when configuration changes; checklist completion is not automatic installation or operational certification.

Access fulfillment and notification operations (1.8)#

Request approval remains a review decision. Administrators separately record manual delivery and private instructions; an owner can then confirm installation. Installed is self-reported. Reversing approval clears delivery and installation evidence. No repository invitation, archive download or software access grant is automated.

Access notifications use a separate generic private inbox. Email defaults off per account; owners and administrators opt in from their access inbox. Enable MARKETING_ENABLED on both the official web service and its queue worker; ordinary installations retain false. Queue jobs contain notification IDs and recheck the recipient, verification, ban state, current permissions/preferences and request revision/existence before submitting mail. Deleted requests cascade their alerts. Internal notes, proposals and delivery instructions are never included in mail or notification payloads.

Provider acceptance is not inbox receipt. Email claims prevent concurrent duplicate attempts; an uncertain attempt may have succeeded. Owners can explicitly retry eligible failed/unknown alerts or pending alerts older than ten minutes, up to three times, with a duplicate warning. Opt-in does not guarantee delivery; monitor the configured mail transport and worker as for existing application mail. No historical alert backfill occurs on upgrade.

Customer-story publication and screenshots (1.8)#

Verified members submit their own story and explicitly consent to publication of the supplied title, link, body, quote and screenshot. Submissions stay private until administrator review. Owners can edit, withdraw or delete; edits require renewed consent and reset publication. Administrators moderate a decision and response without rewriting customer testimony. Public pages expose approved consented fields only; owner addresses, review metadata and private file paths remain private.

Screenshots are bounded JPEG/PNG/WebP uploads, decoded and re-encoded to WebP without original metadata, stored on the private uploads disk. Media authorization is checked on every request; responses use no-store and nosniff. Withdrawal removes public story/media access immediately; explicit story deletion removes its screenshot after transaction commit. Previously downloaded copies cannot be recalled.

The command php artisan stories:prune-screenshots cleans unreferenced UUID screenshots older than 24 hours, including orphans after a manual account deletion cascade or interrupted submission. It retains referenced/recent files and ignores other folders or names. Add --dry-run to count them without deleting. The web container runs it in report-only mode daily (see Container, users and file clean-up); it does not delete story records. Real stories require real customer material and consent; the empty showcase contains no invented testimonials.

Container, users and file clean-up#

The production image runs the web server, queue worker and schedulers as an unprivileged app user (uid 10001), not root. Application code is read-only to that user; only storage/ and bootstrap/cache/ are writable. The image still starts as root for a moment so deploy/drop-privileges.sh can hand a freshly mounted uploads volume to the app user (platform volumes are mounted root-owned, and files written by older root-run releases keep root ownership until this runs once). Web and worker both do this themselves, so a platform start command such as sh deploy/worker.sh needs no change. Caddy's state lives under /tmp, not the volume.

A shell opened by the platform (for example Railway SSH) is root. Run maintenance through the same helper so it does not leave root-owned files behind: sh deploy/drop-privileges.sh php artisan forum:install.

FrankenPHP and Composer are pinned to an exact version and digest in the Dockerfile. Dependabot proposes monthly bumps for those and for the GitHub Actions used in CI.

Only the web service mounts the uploads volume, so the file clean-up runs there (deploy/web-scheduler.sh, daily, first pass ten minutes after start). By default it only reports: the log shows how many unreferenced upload files and orphaned story screenshots exist, with file names for uploads. Nothing is removed until you set FORUM_FILE_PRUNE_DELETE=true on the web service, after which it runs uploads:prune --delete and stories:prune-screenshots. Both commands only touch files older than 24 hours that no database row references. Without that script, run them yourself from a host scheduler on the machine that holds the uploads.

Content-Security-Policy#

HTML pages carry a Content-Security-Policy that limits where scripts, styles, frames and form posts may come from. It starts in report-only mode: browsers log what the policy would block to POST /csp-report (one csp.violation warning per report, with query strings removed) and block nothing. Set FORUM_CSP_MODE to enforce to start blocking, or off to send no policy. Read the log for a few weeks of real traffic, including the admin area, your theme and any plugins, before enforcing.

The policy allows only this site's own scripts and stylesheets, one fixed inline script (the saved light or dark appearance), 'unsafe-eval' (the Alpine library evaluates the page's x-data and @click expressions), Cloudflare Turnstile, and images from any HTTPS site. Form posts may go to this site, Stripe Checkout and Stripe billing, and PayPal, because a redirect after a post is checked too. Attachment downloads keep their own stricter policy. The policy is not sent while a Vite development server is running (public/hot).

Typical report-only findings are a theme or plugin with an inline <script>, <style>, style= attribute or onclick= handler, or one that loads a font or script from another site. Move that code into a theme asset, or tell the theme author before you enforce.

Keeping a test site out of search engines#

Set FORUM_NOINDEX=true to add X-Robots-Tag: noindex, nofollow, noarchive to every response and make /robots.txt disallow everything. Remove the variable (or set it to false) for launch; robots.txt is cached for five minutes. Search engines that cannot fetch a page because robots.txt blocks it may never see its noindex header, so a page that was already indexed can take longer to drop out.