# Deployment and operations

This is a local implementation, not a verified production deployment. The owner has selected NixiHost shared hosting, a Free + Premium launch and [www.swedenstart.com](https://www.swedenstart.com) as the production domain. Account capabilities, mail, backup and monitoring settings and actual processing locations still need verification. See [the NixiHost runbook](nixihost.md). Obtain legal/tax review before allowing consumer sales.

## Release preparation

1. Use PHP 8.5 and the lockfiles. Run `composer install --no-dev --prefer-dist --no-interaction --no-plugins --optimize-autoloader`, `npm ci --ignore-scripts`, `npm run build`. Neither operation needs Git history. Serve **only `public/`** through the web server and PHP-FPM; deny dotfiles, environment files and private storage. Do not expose the repository root or a PHP development server.
2. Provide a new production `APP_KEY`, stable independent `DELETION_HMAC_KEY`, MySQL credentials, `APP_URL=https://www.swedenstart.com`, mail transport and sender, dedicated Stripe sandbox credentials while validating, and webhook secret. Keep all server secrets outside Vite variables and release artifacts. Use a secret manager/protected environment. Retain encryption keys securely for required financial records.
3. Set `APP_ENV=production`, `APP_DEBUG=false`, `SESSION_SECURE_COOKIE=true`, encrypted database sessions, database queues/cache, bounded sessions, and approved host/proxy configuration. Host validation reads the cached application URL and matches only that exact hostname. Set `TRUSTED_PROXIES` only to known ingress IPs/CIDRs, or leave it empty for direct HTTPS. Verify HTTPS and secure cookies before allowing HSTS; production HTTPS responses add HSTS only after the launch gate is approved.
4. Verify Terms/Privacy/Cookies against actual processors, retention, locations, transfers, rights handling and business identity. Replace the development drafts and their version identifiers after review; setting booleans does not finalize the documents. Set the operator fields and approval flags **only after** the underlying decisions are complete. `php artisan swedenstart:launch-check --with-billing --check-database` returns nonzero for unresolved gates, unsafe settings, missing built assets, nondelivering mail, database failures and pending migrations. It prints no secret values and changes no database state. This is a configuration check, not legal approval or proof of external service delivery. `LAUNCH_APPROVED=false` blocks public production web access.
5. Back up the database before `php artisan migrate --force`. Never run `migrate:fresh` on service data. Seed with `php artisan db:seed --force`: existing source edits and user task snapshots are preserved. Run `php artisan optimize` only after verifying the complete environment. Restart workers after release (`php artisan queue:restart`).

## Processes and monitoring

Run one supervised `php artisan queue:work database --queue=default --tries=5 --timeout=90 --backoff=60` process initially. Restart on failure and release; supervise memory and job age. Ensure its process-stop grace exceeds the job timeout. Laravel's database queue retry interval must exceed the worker timeout (configure it to at least 120 seconds before production). Keep database connections and PHP resources bounded.

On shared hosting, set `QUEUE_WORKER_MODE=cron` instead of assuming a supervised daemon is available. The scheduler then drains up to 25 queued jobs per tick, stopping when empty or after a 45-second work budget, with a 90-second per-job timeout and a three-minute overlap lock. The final job can extend the worker to roughly 135 seconds, plus scheduler/maintenance overhead; confirm the actual invocation budget and CLI `pcntl` availability with the host and measure under expected load. A shared cron interval also limits activation, email and refund processing latency. Do not run both worker modes together unintentionally. The NixiHost template uses its published 15-minute interval rather than assuming one-minute cron is approved. `swedenstart:reconcile` queues per-purchase reconciliation jobs instead of running an unbounded series of external API calls inside the scheduler; a worker must process these jobs.

Run `php artisan schedule:run` once a minute with the production PHP binary and application working directory. The schedule checks deferred activation/expiry and non-SFI reminders each minute, canonical billing reconciliation hourly, queue backlog every five minutes, and expired reset tokens daily. Scheduler overlap uses database locks. Access policies independently check entitlement/expiry on requests and queued reminder delivery.

Monitor `/up`, an authenticated app smoke check, database availability/storage, queue latency, `failed_jobs`, Stripe event state `failed`, pending/failed/refund-deadline exceptions, mail delivery and disk space. `queue:monitor` emits a Laravel queue-busy event; connect it and supervisor failures to the owner's actual alert destination. No external alert provider is currently configured. Do not mark a pending refund complete because a worker completed.

Use `php artisan queue:failed`, investigate the opaque record ID, then `queue:retry <uuid>` after correcting the cause. Queue failures store a redacted exception class and ID-bearing job payload, rather than raw private forms. Application exception logs strip raw exceptions/context. Configure access/rotation/retention with the approved policy, and keep web-server logs from recording query strings containing verification/invitation/reset links. Never enable mail logging in production.

## Backups, restore and rollback

Encrypt backups with separately controlled credentials. Restrict access, monitor successful runs and perform a real restore rehearsal before launch. The owner must approve frequency, location and retention; no unverified promise is made in the public policy.

Keep an authoritative deletion ledger independently of point-in-time database backups, with the stable HMAC key. Restore into an isolated, access-blocked environment, merge the current deletion ledger, run `php artisan swedenstart:apply-deletions`, and verify deleted accounts, sessions, tasks, sharing and queued task messages cannot return. Reconcile Stripe against canonical current state before enabling workers or public traffic. A restored paid/withdrawn record must not reactivate a contract whose notice was received later.

Retain the last tested release and build manifest. Prefer an application rollback with forward-compatible schema; do not blindly roll back migrations that contain financial or user records. Stop selling while resolving inconsistent payments. Preserve withdrawal notices and their original received times across incidents, restore and rollback.

## Administrator access

Create a normal verified account, enable and confirm Fortify TOTP, then an operator may run `php artisan swedenstart:make-admin <email>`. No public admin registration or fixed admin password exists. `/admin` requires verified email, confirmed MFA, reauthentication and policy authorization. Administrators cannot disable their MFA through the HTTP endpoint. The UI offers source/guide review, versioned templates, minimal suspension and billing exceptions; it does not browse private relocation profiles.
