# Deployment Guide

## Requirements

- Docker 24.0+
- Docker Compose 2.20+
- A reverse proxy that terminates TLS for the public hostname
- A writable checkout directory for `./data` and `./content`

## Fresh Deployment

```bash
git clone https://github.com/your-org/cairnquire.git
cd cairnquire
cp .env.example .env
```

Edit `.env` and set the HTTPS origin that users will open in their browser:

```dotenv
CAIRNQUIRE_PUBLIC_ORIGIN=https://cairnquire.example.com
```

Start the application and verify the local container health endpoint:

```bash
docker compose up -d --build
docker compose ps
curl http://127.0.0.1:8080/health
```

Compose starts Mailpit for local SMTP capture. Open `http://127.0.0.1:8025`
to inspect password-reset and notification messages. SMTP is also exposed on
`127.0.0.1:1025` for host-run development servers.

Then open `https://cairnquire.example.com/setup`. On an empty database the
first-run wizard:

1. Creates the initial administrator account.
2. Sets its password.
3. Records whether visitors may create their own accounts.
4. Signs the administrator in.

The setup endpoint is only available until the first administrator has been
created. The signup setting can be changed later from the administrator's
account page.

## Environment Variables

The checked-in Compose file sets the container paths. For a normal single-host
deployment, only `CAIRNQUIRE_PUBLIC_ORIGIN` must be supplied in `.env`.

| Variable | Required | Compose default | Description |
|----------|----------|-----------------|-------------|
| `CAIRNQUIRE_PUBLIC_ORIGIN` | Yes | None | Exact external HTTPS origin. Passkeys and device-flow URLs depend on this value. |
| `CAIRNQUIRE_EMAIL_PROVIDER` | No | `smtp` | Outbound provider: `smtp`, `postmark`, `cloudflare`, or `noop`. |
| `CAIRNQUIRE_EMAIL_SMTP_HOST` | No | `mailpit` | SMTP server used for notifications. |
| `CAIRNQUIRE_EMAIL_SMTP_PORT` | No | `1025` | SMTP server port. |
| `CAIRNQUIRE_EMAIL_POSTMARK_TOKEN` | For Postmark | Empty | Postmark server token. |
| `CAIRNQUIRE_EMAIL_CLOUDFLARE_ACCOUNT_ID` | For Cloudflare | Empty | Account with Cloudflare Email Sending enabled. |
| `CAIRNQUIRE_EMAIL_CLOUDFLARE_API_TOKEN` | For Cloudflare | Empty | API token with Email Sending permission. Store it as a deployment secret. |
| `CAIRNQUIRE_EMAIL_FROM` | No | `notifications@cairnquire.local` | Notification sender address. |
| `CAIRNQUIRE_LOG_LEVEL` | No | `INFO` | Application log level. |

The server also accepts these lower-level overrides when it is run outside the
checked-in Compose deployment:

| Variable | Default | Description |
|----------|---------|-------------|
| `CAIRNQUIRE_SERVER_ADDR` | `:8080` | Listen address. |
| `CAIRNQUIRE_DATABASE_PATH` | `../../data/db.sqlite` | Local SQLite/libsql database path. |
| `CAIRNQUIRE_DATABASE_PRIMARY_URL` | Empty | Optional remote libsql primary URL for embedded-replica mode. |
| `CAIRNQUIRE_DATABASE_AUTH_TOKEN` | Empty | Optional auth token for the remote libsql primary. |
| `CAIRNQUIRE_CONTENT_SOURCE_DIR` | `../../content` | Writable Markdown source directory. |
| `CAIRNQUIRE_CONTENT_STORE_DIR` | `../../data/files` | Content-addressed attachment store. |
| `CAIRNQUIRE_CONFIG` | Empty | Optional JSON configuration file. Environment variables override it. |
| `CAIRNQUIRE_DEV_MODE` | `false` | Local-only auth shortcut. Never enable this on a deployed server. |

### Cloudflare Email Service

Cairnquire is an external Go application, so it uses Cloudflare Email
Service's REST API rather than a Workers binding. Before enabling the provider:

1. Enable Email Sending for the sender domain in Cloudflare, or run `npx wrangler email sending enable example.com`.
2. Create a Cloudflare API token with Email Sending permission for the account.
3. Set `CAIRNQUIRE_EMAIL_PROVIDER=cloudflare`, the account ID, API token, and a `CAIRNQUIRE_EMAIL_FROM` address on the onboarded domain.

Cloudflare Email Service is used only for transactional notifications. API
tokens must be supplied through deployment secrets and must not be committed.
The durable queue retries Cloudflare `429` and server errors; validation and
permanent-bounce responses are retained immediately as failed deliveries.

Cloudflare Email Routing for inbound comment replies is a separate Milestone 4
feature and is not enabled by these outbound settings.

## Persistent Data

Compose mounts:

- `./data:/workspace/data` for the database and attachments
- `./content:/workspace/content` for editable Markdown source

Both mounts must remain writable because uploads, browser edits, and sync
writebacks modify them.

## Reverse Proxy

Expose container port `8080` through a TLS-terminating reverse proxy. Preserve
the original hostname and forwarded client address headers. The configured
`CAIRNQUIRE_PUBLIC_ORIGIN` must exactly match the external origin, including
`https://`.

## Backup And Restore

Stop writes or stop the app container before copying a fully consistent local
database backup:

```bash
docker compose stop app
tar czf cairnquire-backup.tgz data content
docker compose up -d app
```

Restore by stopping the app, replacing `data` and `content` from the archive,
and starting the app again.

## Upgrade

```bash
docker compose stop app
tar czf cairnquire-backup-before-upgrade.tgz data content
git pull
docker compose up -d --build
curl http://127.0.0.1:8080/health
```

Database migrations run automatically on startup.

## Troubleshooting

```bash
docker compose logs -f app
docker compose ps
curl http://127.0.0.1:8080/health
```

If passkey creation fails after deployment, confirm that the browser URL and
`CAIRNQUIRE_PUBLIC_ORIGIN` are the same HTTPS origin.
