# Deployment

How to put ABACO INFRA live and keep it running. Written for the person doing it at two in the
morning, so every step says what it is for and what happens if it is skipped.

## What the application needs

| | |
| --- | --- |
| PHP | 8.2 or newer, with `pdo_mysql`, `gd` or `imagick`, `fileinfo`, `mbstring`, `zip`, `intl` |
| MySQL | 8.0 or newer (5.7 works; the `json` columns need it) |
| Node | 20 or newer, for the asset build only — not at runtime |
| Mail | SMTP credentials that can send from the site's own domain |
| Cron | One entry, see **Scheduler** |
| Worker | One queue process, see **Queue** |
| Disk | Two writable trees: `storage/` and `bootstrap/cache/` |

`gd` or `imagick` is not optional. Every upload is re-encoded rather than trusted, and without an
image library the upload service refuses the file rather than storing something it could not read.

## First deployment

```bash
composer install --no-dev --optimize-autoloader
npm ci && npm run build

cp .env.example .env          # then edit it — see Environment below
php artisan key:generate
php artisan storage:link

php artisan migrate --force
```

Then seed, in this order. The order matters: roles have to exist before the accounts that hold them,
and the taxonomy before anything that classifies itself against it.

```bash
php artisan db:seed --class=RolePermissionSeeder
php artisan db:seed --class=UserSeeder
php artisan db:seed --class=SettingSeeder
php artisan db:seed --class=SectorSeeder
php artisan db:seed --class=StructureSeeder
php artisan db:seed --class=AmenitySeeder
php artisan db:seed --class=LocationSeeder
php artisan db:seed --class=MarketplaceTaxonomySeeder
php artisan db:seed --class=PlanSeeder
php artisan db:seed --class=MenuSeeder

# Optional: the verticals beyond property. Leave it out to launch with
# property alone — nothing depends on it.
php artisan db:seed --class=AdditionalSectorsSeeder
```

**Do not run `DemoContentSeeder` or `DemoPropertySeeder`.** They hold placeholder projects, clients,
statistics and listings for design review, and both refuse to run when `APP_ENV=production` — but
running them against a staging copy that is about to become production is the same mistake one step
earlier.

`PlanSeeder` is not optional even if nothing is being sold yet. `SubscriptionService::planFor()`
falls back to the default plan, so without it a seller is on no terms at all rather than on the free
tier, and the listing allowance has nothing to read.

Finally, cache the configuration:

```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

`config:cache` is the step that makes `env()` return null outside config files. Nothing in this
application calls `env()` outside `config/`, and it must stay that way — the one time it did,
`UserSeeder` silently seeded the documented fallback password instead of the one in the environment.

## Environment

`.env.example` documents every key with the reasoning beside it. The ones that are easy to get wrong:

- **`APP_DEBUG=false`.** A stack trace on a production error page shows the database name, the file
  layout and often a query with its bindings.
- **`APP_INDEXABLE`.** Leave it `true` in production and set it `false` on staging. `robots.txt` is
  generated from it, so a staging copy cannot be indexed because somebody forgot to edit a file.
- **`ADMIN_PASSWORD`, `CONTENT_EDITOR_PASSWORD`, `MEDIA_MANAGER_PASSWORD`.** Set these before
  seeding. `UserSeeder` falls back to `ChangeMe!2026` with a warning, and a warning in a deployment
  log is a warning nobody reads.
- **`PAYMENTS_BANK_*`.** Leave them empty and bank transfer hides itself from checkout rather than
  offering a payer instructions the site cannot fulfil. That is the correct behaviour before an
  account exists, and a broken checkout after one should.
- **`RECAPTCHA_*`.** Optional. Public forms keep their honeypot, their time-on-form check and their
  rate limits either way; reCAPTCHA is layered on top only when both keys are present.

## Queue

Notifications, saved-search digests and search matching are queued. Without a worker they never
send, and nothing on screen says so — the mail simply does not arrive.

```
# supervisor, or the equivalent on the host
php artisan queue:work --queue=default --tries=3 --max-time=3600
```

`--max-time` matters more than it looks: a long-lived PHP process accumulates memory, and restarting
it hourly is cheaper than diagnosing why it stopped after a fortnight.

After every deployment:

```bash
php artisan queue:restart
```

Workers hold the old code in memory otherwise, and will keep running it until they happen to exit.

## Scheduler

One cron entry runs everything:

```
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
```

What it drives, and what breaks without it:

| Job | Without it |
| --- | --- |
| `marketplace:expire-listings` | Expired listings keep saying "Published" on the seller's dashboard while nobody can find them |
| `marketplace:sweep-promotions` | A listing stays featured after its promotion ends |
| `marketplace:renew-subscriptions` | Nobody is asked to renew, and nothing ever expires |
| Saved-search digests | Daily and weekly alerts never go out |
| Offer expiry | An unanswered offer stands for ever |

## Deploying an update

```bash
php artisan down --render=errors::503

git pull
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force

php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan queue:restart

php artisan up
```

Take a database dump before any deployment that includes a migration. `migrate --force` does not ask,
and several of this application's migrations drop or rewrite columns on the way down.

## After going live

- **Sign in as each seeded account once** and change its password. The seeded ones are known to
  anybody who has read the repository.
- **Check `/robots.txt` and `/sitemap.xml` actually respond.** Both are generated, so a routing or
  caching mistake shows up as a 404 rather than as a stale file.
- **Send one test enquiry** through a real listing page. It exercises the whole chain at once: the
  form, the honeypot, the rate limit, the lead, the queue and SMTP.
- **Watch `storage/logs` for the first day.** Upload rejections and mail failures both land there and
  nowhere else.

## Backups

Three things need backing up, and only the first is obvious:

1. **The database.** Everything except files.
2. **`storage/app/public`** — every uploaded image and document the public site serves.
3. **`storage/app/private`** — verification documents, listing deeds, message attachments. This is
   the most sensitive data the application holds. It must be backed up, and the backup must be
   treated as carefully as the original: a citizenship certificate in an unencrypted bucket is the
   same exposure as one on the open web.

`storage/framework` and `bootstrap/cache` are rebuilt on deployment and need no backup.

## Health check

`/up` answers 200 when the framework has booted. It does not check the database, the queue or the
mail server, so it tells a load balancer whether to route traffic and tells an operator very little
else. For a real check, watch that the scheduler has run in the last few minutes and that the queue's
`jobs` table is not growing without bound.
