# ClearFin GmbH Website

Next.js 15 (App Router) company website for **https://www.clearfin.ch**.
GitHub: https://github.com/mendlme/clearfinch (private) · Repo owner: `mendlme`.

This guide consolidates the setup notes from the previous Claude installation
(originally `archive/clearfin/website-setup.md`) plus the current project layout.

## Stack

- Next.js 15 (App Router, React 19), TypeScript, Tailwind CSS v4
- `nodemailer` for the contact form (pipes to the local msmtp relay)
- Playwright for tests (pages, links, contact form)
- `output: "standalone"` — deployed as a self-hosted Node server (not Vercel)

## Local development

```bash
npm install               # or: npm ci
npm run dev               # http://localhost:3000
npm run build:standalone  # production build + standalone assembly
npm test                  # Playwright suite (runs build:standalone first)
npm run test:unit         # pure-function tests only, no server
npm run smtp:check        # direct-SMTP fallback only: authenticate, send nothing
```

The contact form reads `.env.local` (gitignored); copy from `.env.example`. It
needs no credentials when piping to a local msmtp — see Mail below.

The test suite is hermetic: each project boots the real `.next/standalone`
bundle on an ephemeral port behind a local fake relay, so no credentials and no
network are required, and "the form works" means something actually accepted
the message.

| Project  | What it covers                                                     |
|----------|--------------------------------------------------------------------|
| `unit`   | Config parsing, payload validation, HTML/header escaping            |
| `api`    | The contact API end-to-end, over both the pipe and direct SMTP      |
| `deploy` | Config reaching the server the way the deployed one receives it     |
| `e2e`    | Browser flows: pages, links, the contact form                       |

## Branch & deploy model

```
main branch  -->  Build & Test (GitHub CI)  -->  Deploy  -->  znik.org/clearfintest  (port 3001, test)
prod branch  -->  Build & Test (GitHub CI)  -->  Deploy  -->  www.clearfin.ch         (port 3000, prod)
```

Promote test → production:

```bash
git checkout prod && git merge main && git push origin prod
```

CI runs the Playwright suite before either environment deploys. Deploy jobs run
on a **self-hosted GitHub Actions runner** on the VPS (GitHub-hosted IPs are
firewalled from the VPS SSH port), invoking the deploy script on the server.

## basePath (important gotcha)

`next.config.ts` sets `basePath` from `NEXT_PUBLIC_BASE_PATH`, defaulting to
`/clearfintest` when the var is unset. Production must build with the var set to
an **empty string** so the site serves at the domain root:

- Prod deploy (`deploy-prod.sh`) and `ci-prod.yml` both set `NEXT_PUBLIC_BASE_PATH=''`.
- Never hardcode `/clearfintest` in fetch URLs or asset paths — derive from the
  env/basePath (past bugs: contact-form fetch URL, favicon URLs, founder image).

## VPS layout (vps557559 · 193.70.39.121 · Debian)

| What          | Path                             |
|---------------|----------------------------------|
| Test app      | `/var/www/znik.org/clearfinch/`  |
| Prod app      | `/var/www/clearfin.ch/app/`      |
| Test service  | `clearfinch.service` (port 3001) |
| Prod service  | `clearfinch-prod.service` (port 3000) |
| GitHub runner | `/opt/github-runner` (runs as `mendl`) |

Both apps run as systemd services behind an Apache reverse proxy. Apache serves
`/email/` directly (legacy assets used in transactional emails). Server repos
have `git config core.fileMode false` to avoid chmod-only diffs.

## Mail (important gotcha)

The contact form does **not** hold mail credentials. Every host already runs
msmtp as a sendmail-compatible relay, so the app pipes to `/usr/sbin/sendmail`
and a provider password rotation never touches this deployment.

Enquiries are sent as **`infra@markusendl.com`** (Migadu), not from a
`clearfin.ch` address. The mail is a notification to its own owner with the
visitor in `Reply-To`, so no outsider sees the `From`, and keeping machine mail
off the business domain keeps cron and alert traffic away from its reputation.
If the site ever mails a *visitor* — a confirmation, a receipt — that needs a
real `clearfin.ch` mailbox instead, for DMARC alignment.

The full picture, including which provider holds which domain and how the
credential is rotated, lives in `docs/MAIL.md` of the **scriptbase** repo.

| Transport  | When                                    | Needs                          |
|------------|-----------------------------------------|--------------------------------|
| `sendmail` | The servers. Default when no `SMTP_HOST`| `SMTP_FROM`, `CONTACT_TO`      |
| `smtp`     | Direct to a mail server; local fallback | Those plus `SMTP_HOST/USER/PASS` |

`MAIL_TRANSPORT` forces one; unset, the presence of `SMTP_HOST` decides. In CI
and the test suite msmtp is stood in for by a script that records its argv and
stdin, so the pipe is exercised for real.

`.next/standalone/server.js` calls `process.chdir(__dirname)`, so Next loads
`.env*` **relative to `.next/standalone/`, not the repo root**. A `.env.local`
sitting at the repo root is invisible to the running server: every page renders
and only sending fails. This is what broke the contact form on both
environments. `npm run build:standalone` (`tools/prepare-standalone.mjs`)
therefore copies `.next/static`, `public/` **and `.env.local`** into the
bundle; `tests/deploy/` fails if that copy stops happening.

Two checks, deliberately separate:

- `GET /api/health/mail` — can this server send? Reports the transport, names
  any missing env vars (never values), and for the pipe confirms the binary
  exists and is executable by the service. Both deploy scripts fail on a bad
  result. Nothing is sent.
- Credentials are msmtp's business: `msmtp --serverinfo` on the host. For the
  direct-SMTP fallback, `npm run smtp:check [envfile]` authenticates without
  sending.

The `.env.local` both environments need:

```
MAIL_TRANSPORT=sendmail
SMTP_FROM=infra@markusendl.com
CONTACT_TO=markus.endl@clearfin.ch
```

## GitHub Actions secrets (repo settings)

| Secret | Purpose |
|--------|---------|
| `MAILTRAP_HOST` / `_USER` / `_PASS` / `_FROM` / `_TO` | Legacy — no longer used; CI mail tests run against a local fake relay |
| `VPS_HOST` / `VPS_USER` / `VPS_SSH_KEY` | Legacy — unused (replaced by self-hosted runner) |

## Security notes (from previous setup — action items for the owner)

1. A GitHub PAT was previously hardcoded in the old repo's git remote URL
   (in `archive/clearfinch/.git/config`). This fresh clone uses the `gh`
   credential helper instead — no token in the remote. **Revoke that old PAT**
   at github.com/settings/tokens.
2. A malicious `postinstall` backdoor was once committed (commit `757cc14`) and
   later removed; current `package.json` is clean. Owner should **rotate the
   GitHub credentials and the Titan SMTP password** if not already done.

## Archive

`../archive/` holds the previous installation: `clearfinch/` (the old working
copy — superseded by this clone) and `clearfin/` (brand assets: logo SVGs,
founder photos, and the original `website-setup.md`). Brand SVGs already live in
`public/images/`.
