# Sources & evidence

Companion to [`index.html`](index.html). Every non-trivial technical claim in the article was checked against current source on 2026-05-18. SHAs below pin the commits inspected.

| Repo | Commit |
|---|---|
| `nextcloud/app_api` | `2c9093f6c04a` |
| `cloud-py-api/nc_py_api` | `69e040664424` |
| `nextcloud/app-skeleton-python` | `0127c88f98a0` |
| `nextcloud/HaRP` | `067c09f96ac0` |
| `fatedier/frp` | `4ec8de973f52` |

## Claim → source

### 1. The proxied request from Nextcloud to an ExApp carries `AA-VERSION`, `EX-APP-ID`, `EX-APP-VERSION`, `AUTHORIZATION-APP-API`, `AA-REQUEST-ID` (and nothing else canonical).

Constructed here:

`app_api/lib/Service/AppAPICommonService.php` lines 27–31:

```php
'AA-VERSION' => $this->appManager->getAppVersion(Application::APP_ID, false),
'EX-APP-ID' => $exApp->getAppid(),
'EX-APP-VERSION' => $exApp->getVersion(),
'AUTHORIZATION-APP-API' => base64_encode($userId . ':' . $exApp->getSecret()),
'AA-REQUEST-ID' => $request instanceof IRequest ? $request->getId() : 'CLI',
```

Verified on the receiving side in `nc_py_api/_session.py` (`sign_check`), which reads `EX-APP-ID`, `EX-APP-VERSION`, `AUTHORIZATION-APP-API` and extracts `(username, secret)` from the base64-decoded auth header. No `AA-SIGNATURE` or `AA-USER-ID` header is present in the production flow.

### 2. The three access levels are `PUBLIC`, `USER`, `ADMIN`. Nothing else.

`app_api/lib/Controller/ExAppProxyController.php`, `passesExAppProxyRouteAccessLevelCheck`:

```php
return match ($accessLevel) {
    ExAppRouteAccessLevel::PUBLIC->value => true,
    ExAppRouteAccessLevel::USER->value   => $this->userId !== null,
    ExAppRouteAccessLevel::ADMIN->value  => $this->userId !== null && $this->groupManager->isAdmin($this->userId),
    default => false,
};
```

The `default => false` branch makes any other value a hard reject. There is no `GUEST` level in production. (One reviewer suggested otherwise; the source disagrees.)

### 3. `set_handlers` registers two lifecycle routes: `PUT /enabled` and `POST /init`.

`nc_py_api/ex_app/integration_fastapi.py` lines 127–152:

```python
@fast_api_app.put("/enabled")
async def enabled_callback(...):
    return JSONResponse(content={"error": await enabled_handler(enabled, nc)})

...

@fast_api_app.post("/init")
async def init_callback(...):
    ...
```

The `/init` route is opt-out (`default_init=False`), `/enabled` is always registered. Both must respond 200 for the install to succeed.

### 4. HaRP is the current AppAPI tunnel; uses FRP under the hood.

- `nextcloud/HaRP` — repo description: "Fast Proxy for AppAPI(Nextcloud 32+)".
- `fatedier/frp` — README: "A fast reverse proxy to help you expose a local server behind a NAT or firewall to the Internet." Written in Go, primary author `fatedier`.

### 5. GPU support is daemon-side, with image-tag suffixing and DeviceRequests/CDI wiring done at install time.

`app_api/lib/DeployActions/DockerActions.php`:

- Image tag rewrite at install (line ~463):

  ```php
  return $imageParams['image_src'] . '/'
      . $imageParams['image_name'] . ':' . $imageParams['image_tag']
      . '-' . $daemonConfig->getDeployConfig()['computeDevice']['id'];
  ```

- CUDA wiring (lines ~544–567) — Docker `HostConfig.DeviceRequests`, or on Podman 5.4+ CDI selectors (default `nvidia.com/gpu=all`):

  ```php
  $selectors = $params['cdiDevices'] ?? ['nvidia.com/gpu=all'];
  $containerParams['HostConfig']['DeviceRequests'] = [[
      'Driver' => 'cdi',
      'DeviceIDs' => $selectors,
  ]];
  ```

- ROCm wiring (lines ~569–575) — bind-mounts `/dev/kfd` and `/dev/dri`.

### 6. `COMPUTE_DEVICE` env var is injected, uppercased.

Same file, lines 1179–1180:

```php
// Always set COMPUTE_DEVICE=CPU|CUDA|ROCM
$autoEnvs[] = sprintf('COMPUTE_DEVICE=%s', strtoupper($deployConfig['computeDevice']['id']));
```

Note the uppercasing — the article was originally wrong here (had it lowercase).

### 7. TLS between ExApp's `frpc` and HaRP is conditional on `/certs/frp` being mounted.

`app-skeleton-python/start.sh`, lines 10–46:

```bash
if [ -d "/certs/frp" ]; then
    echo "Found /certs/frp directory. Creating configuration with TLS certificates."
    cat <<EOF > /frpc.toml
    ...
    transport.tls.enable = true
    transport.tls.certFile = "/certs/frp/client.crt"
    ...
EOF
else
    echo "Directory /certs/frp not found. Creating configuration without TLS certificates."
    cat <<EOF > /frpc.toml
    ...
    transport.tls.enable = false
    ...
EOF
fi
```

mTLS is the certs-mounted path; the fallback is plain FRP with token auth only. The article previously claimed mTLS unconditionally — corrected.

### 8. Named AI ExApps verified.

- `nextcloud/context_chat` — info.xml contains `<external-app>` block. ✓
- `nextcloud/llm2` — info.xml: `<external-app><docker-install><image>nextcloud/llm2</image>...`. ✓
- `nextcloud/stt_whisper2` — verified ExApp. (The article previously named `integration_whisper`, which does not exist.) ✓
- `cloud-py-api/visionatrix` — info.xml: `<name>Visionatrix</name><external-app>...`. ✓

### 9. `nodejs-app-skeleton` and `golang-app-skeleton` do not exist.

`gh repo view cloud-py-api/nodejs-app-skeleton` and `gh repo view cloud-py-api/golang-app-skeleton` both return "Could not resolve to a Repository." `gh search repos --owner=cloud-py-api` and `--owner=nextcloud` confirm only the Python skeleton is officially published. Removed from the article.

## Cross-LLM fact-check

The article was sent to three SOTA models on OpenRouter for an independent fact-check (with the verified facts above held back so we could see if they'd find the same issues):

- **openai/gpt-5.5** — caught: `AA-SIGNATURE`/`AA-USER-ID` are fictional, TLS is conditional, two lifecycle endpoints not one, nonexistent Node/Go skeletons, nonexistent `integration_whisper`, the `/tmp/exapp.sock` vs `$APP_HOST:$APP_PORT` contradiction. Verdict: don't ship before fixing.
- **google/gemini-3.1-pro-preview** — caught the same set, plus suggested a fourth `GUEST` access level (incorrect — source has only three) and questioned the Podman 5.4 threshold (it's confirmed by source). Useful but introduced one false positive.
- **moonshotai/kimi-k2.6** — caught the same set; verdict ship-with-cuts (mostly about prose tightening, not facts).

The intersection of the three confirmed the fixes you see applied. Disagreements were resolved by reading the source directly.

## What I did NOT verify

- Whether `context_chat`, `llm2`, `stt_whisper2`, `visionatrix` all *require* a GPU at runtime — only that they are ExApps and publish AI-flavoured functionality. Article now says "AI-flavoured" rather than "GPU-flavoured" for that reason.
- The history claim "Nextcloud started life as an ownCloud fork in 2016" — that opener was cut on independent editorial grounds (all three reviewers flagged it as throat-clearing), so the question is moot in the published version.
