# CrossCheck server configuration

The PHP API under `api/` reads settings from **environment variables** (what `getenv()` returns). You can set those the usual way for your stack, **or** use a small PHP file next to the API if that is easier.

## Quick path: `api/config.local.php`

1. Copy the template:

   ```bash
   cp api/config.local.example.php api/config.local.php
   ```

2. Edit `api/config.local.php` and uncomment or add the keys you need (see below). Use plain strings.

3. Lock down permissions (secrets on disk):

   ```bash
   chmod 640 api/config.local.php
   chown www-data:www-data api/config.local.php
   ```

   Adjust `www-data` to whatever user your PHP runs as.

4. Ensure the web server can write the database directory:

   ```bash
   mkdir -p data
   chown www-data:www-data data
   chmod 775 data
   ```

On each request, `_bootstrap.php` loads `config.local.php` **first** and only applies a key when it is **not** already set in the real environment. So **Apache / PHP-FPM / systemd** values override the file if both exist.

## Variables (reference)

| Variable | Required? | Purpose |
|----------|-----------|---------|
| `CROSSCHECK_SLACK_WEBHOOK` | For Slack alerts | Incoming Slack webhook URL. Posted when someone publishes. |
| `CROSSCHECK_SLACK_TAKEDOWN_WEBHOOK` | No | Optional second incoming webhook used only for **review / takedown** requests from `browse.html`. If unset, those alerts reuse `CROSSCHECK_SLACK_WEBHOOK` when it is configured. **Batched thumbs-down** Slack alerts use the same webhook chain. |
| `CROSSCHECK_SLACK_THUMBS_DOWN_INTERVAL` | No | Minimum seconds between batched Slack thumbs-down messages per viewer network (default `5`; minimum enforced `1`). |
| `CROSSCHECK_MODERATE_SECRET` | For Slack quick links + `moderate.php` | Same secret signs Slack moderation URLs and authenticates `api/moderate.php`. |
| `CROSSCHECK_PUBLIC_BASE_URL` | Recommended | Base URL (no trailing slash), e.g. `https://atom.builders/crosscheck`. Used for Slack links; if unset, the first publish may still infer the base from the browser **Referer**. |
| `CROSSCHECK_MODERATION_LINK_TTL_SECONDS` | No | How long Slack “Open moderation page →” links stay valid (default `604800` = 7 days, max about 31 days). |
| `CROSSCHECK_ALLOWED_HOSTS` | No | Comma-separated hostnames allowed on publish APIs (default includes `atom.builders`, `wsams.org`, `localhost`, etc.). |
| `CROSSCHECK_AUTO_APPROVE` | No | Set to `1` only on a **dev** box to skip the moderation queue. |
| `CROSSCHECK_DB_PATH` | No | Full path to the SQLite file (default: `data/crosscheck.sqlite` under the repo). |
| `CROSSCHECK_IP_PEPPER` | No | Secret pepper mixed into stored IP hashes (change in production). |
| `CROSSCHECK_DEBUG` | No | `1` to expose more error detail in JSON (not for production). |
| `CROSSCHECK_MAX_BODY_BYTES` | No | Max JSON body size for publish (default `524288`). |
| `CROSSCHECK_TICKETS_PER_HOUR` | No | Publish-token rate limit per session (default `30`). |

See also `crosscheck.env.example` for the same list in dotenv-style comments.

## Slack: one-click review and approve

When `CROSSCHECK_MODERATE_SECRET` is set and a public base URL is known (`CROSSCHECK_PUBLIC_BASE_URL` or inferred from **Referer** on publish), each **pending** Slack notification includes a link:

**Open moderation page →** → `api/moderation.php?id=…&exp=…&sig=…`

That page shows the stored record (claims, tools, excerpts) and **Approve** / **Reject** buttons. No `curl` required. The link is **signed** with your moderate secret and expires after `CROSSCHECK_MODERATION_LINK_TTL_SECONDS` (default one week).

The JSON API `api/moderate.php` still works for automation (Bearer or `X-Crosscheck-Moderate` header).

## Alternative: real environment variables

### Apache (`mod_php` or PHP as module)

Inside the `<VirtualHost>` (or in `.htaccess` if `AllowOverride` permits and your host allows `SetEnv`):

```apache
SetEnv CROSSCHECK_SLACK_WEBHOOK "https://hooks.slack.com/services/XXX/YYY/ZZZ"
SetEnv CROSSCHECK_MODERATE_SECRET "use-a-long-random-string"
SetEnv CROSSCHECK_PUBLIC_BASE_URL "https://yourdomain.example/crosscheck"
```

Reload Apache after editing.

### PHP-FPM (`php-fpm` pool)

In the pool file (e.g. `/etc/php/8.3/fpm/pool.d/www.conf` or a site-specific pool):

```ini
env[CROSSCHECK_SLACK_WEBHOOK] = https://hooks.slack.com/services/XXX/YYY/ZZZ
env[CROSSCHECK_MODERATE_SECRET] = use-a-long-random-string
env[CROSSCHECK_PUBLIC_BASE_URL] = https://yourdomain.example/crosscheck
```

Restart PHP-FPM.

### systemd (unit that starts PHP or a wrapper)

In the service unit, or a drop-in:

```ini
[Service]
Environment="CROSSCHECK_SLACK_WEBHOOK=https://hooks.slack.com/services/XXX/YYY/ZZZ"
Environment="CROSSCHECK_MODERATE_SECRET=use-a-long-random-string"
```

Or point to a file:

```ini
EnvironmentFile=/etc/crosscheck.env
```

Put `KEY=value` lines in `/etc/crosscheck.env` (no `export` keyword), mode `600`, root-owned.

## Approve a pending publication

After a publish, Slack (if configured) includes the publication `id`. Approve with:

```bash
curl -sS -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-Crosscheck-Moderate: YOUR_MODERATE_SECRET' \
  -d '{"id":"PASTE_32_CHAR_HEX_ID","action":"approve"}' \
  'https://yourdomain.example/crosscheck/api/moderate.php'
```

Use `"action":"reject"` to hide, or `"pending"` to reset.

If `Authorization` is not stripped by your host, you can use:

`-H 'Authorization: Bearer YOUR_MODERATE_SECRET'`

## Slack fired, but nothing appears on Shared runs?

That is normal until someone **approves** the row.

- Every successful publish is stored as **`pending`** unless `CROSSCHECK_AUTO_APPROVE=1`.
- **`browse.html` / `api/feed.php`** only return **`approved`** publications.
- The Slack webhook runs when a publish **succeeds**, including for pending rows — it is **not** “already public.”

Approve the id shown in Slack using `api/moderate.php` (see the curl example above). After that, the post shows on Shared runs and at `browse.html?id=…`.

## Shared runs: Slack alerts for review requests

When someone uses **Request moderator review / takedown** on `browse.html`, the server sends a Slack message (same webhook as publish unless you set **`CROSSCHECK_SLACK_TAKEDOWN_WEBHOOK`**). Configure at least one of those webhooks if you want to receive those alerts. The server stores **one** small SQLite row per hashed network identifier and publication id so the same network cannot spam the button. **Thumbs up/down** on Shared runs are stored in the **`publication_votes`** table (aggregate counts; one current vote per viewer bucket per publication).

When a viewer **newly selects thumbs down** (not a repeat click while already down), the server can send a Slack alert on the same webhook chain as takedown requests. Multiple downs from the same viewer network are **batched** into a single message, with at most **one Slack post per network per 5 seconds** by default (`CROSSCHECK_SLACK_THUMBS_DOWN_INTERVAL`).

## Security notes

- Do **not** commit `api/config.local.php` (it is in `.gitignore`).
- Prefer HTTPS for the site so session cookies stay `Secure`.
- Restrict who can read `config.local.php` on disk (`640`, correct owner).
