docs: Phase 7 — update docs to reflect optional services migration

Email, calendar, and files are now optional store services, not always-on
builtins. Updated README, QUICKSTART, Wiki, and service-developer-guide to
reflect: dynamic nav, optional service install flow, correct egress
identifiers (wireguard_ext/default vs wireguard/cell_internet), removed
builtin/store distinction from manifest reference, 7 core containers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 17:10:48 -04:00
parent 44d7e96f29
commit 10ac15d9fe
4 changed files with 145 additions and 108 deletions
+62 -52
View File
@@ -15,7 +15,8 @@ This guide is for developers who want to build services that integrate with Pers
5. [Backup integration](#5-backup-integration)
6. [Egress routing](#6-egress-routing)
7. [Quick-start example](#7-quick-start-example)
8. [Submitting to the store](#8-submitting-to-the-store)
8. [Reference implementations](#8-reference-implementations)
9. [Submitting to the store](#9-submitting-to-the-store)
---
@@ -29,19 +30,15 @@ A PIC service is a Docker container (or a set of containers) that plugs into the
- Which paths to include in automated backups
- Which outbound network interfaces the service is allowed to use
PIC has two kinds of services:
All PIC services are **store services** — optional packages installed by the cell admin from the `pic-services` catalog. PIC downloads the manifest, renders a per-service Docker Compose file, and starts the containers. The core PIC stack (DNS, DHCP, NTP, WireGuard, Caddy, API, WebUI) runs independently of any installed services.
**Builtins** ship with PIC itself (email, calendar, files). Their containers are declared in the main `docker-compose.yml`. Their manifests live under `api/services/builtins/<id>/manifest.json`. They use Python manager classes for account operations because they run inside the same Docker network and share internal access.
**Store services** are third-party packages installed by the cell admin from the PIC service store. PIC downloads the manifest from the store index, allocates a static IP for the container in the service pool (`172.20.x.x`, offsets 20254 within your `ip_range` subnet), generates a Docker Compose override file, and starts the container. Store services use a small HTTP API (described in section 4) for account operations.
The `ServiceRegistry` in `api/service_registry.py` is the single source of truth for both kinds. `CaddyManager`, the backup system, and the peer services endpoint all read from it rather than from hardcoded lists.
The email, calendar, and files services (in `pic-services/services/`) are the reference implementations and show the full feature set. The `ServiceRegistry` in `api/service_registry.py` is the single source of truth for all installed services. `CaddyManager`, the backup system, and the peer services endpoint all read from it rather than from hardcoded lists.
---
## 2. Manifest reference
The manifest is a JSON file with `"schema_version": 3`. Every field is described below. The three builtin manifests (`email`, `calendar`, `files`) are the canonical examples.
The manifest is a JSON file with `"schema_version": 3`. Every field is described below. The `email`, `calendar`, and `files` manifests in `pic-services/services/` are the canonical reference examples.
### Top-level identity fields
@@ -53,7 +50,7 @@ The manifest is a JSON file with `"schema_version": 3`. Every field is described
| `description` | string | yes | One-sentence description shown in the UI. |
| `version` | string | yes | Semver string for the service package itself (e.g. `"1.0.0"`). |
| `author` | string | yes | Your name or organisation. |
| `kind` | string | yes | `"builtin"` for built-in services, `"store"` for third-party packages. |
| `kind` | string | yes | Must be `"store"`. |
| `min_pic_version` | string | no | Minimum PIC version required (e.g. `"1.0"`). |
```json
@@ -99,7 +96,7 @@ These fields are only read when `has_subdomain` is `true`.
| Field | Type | Required | Description |
|---|---|---|---|
| `subdomain` | string | yes (if `has_subdomain`) | The primary subdomain (e.g. `"notes"`). Results in `notes.<cell-domain>`. Must not collide with reserved names: `api`, `webui`, `admin`, `www`, `mail`, `ns1`, `ns2`, `git`, `registry`, `install`. |
| `subdomain` | string | yes (if `has_subdomain`) | The primary subdomain (e.g. `"notes"`). Results in `notes.<cell-domain>`. Must not collide with reserved names: `api`, `webui`, `admin`, `www`, `ns1`, `ns2`, `git`, `registry`, `install`. |
| `extra_subdomains` | array of strings | no | Additional subdomains that point to the same backend (e.g. `["webmail"]`). |
| `backend` | string | yes (if `has_subdomain`) | The container-name:port combination that Caddy proxies to (e.g. `"cell-notes:8080"`). Uses Docker DNS on the `cell-network`. |
| `extra_backends` | object | no | Maps extra subdomain names to separate backends. Key is the subdomain string; value is the backend string. The email service uses this to send `webdav.*` to a different container than `files.*`. |
@@ -176,7 +173,7 @@ Required when `has_accounts` is `true`.
| Field | Type | Description |
|---|---|---|
| `manager` | string | For builtins: the Python manager name used internally (`"email_manager"`, `"calendar_manager"`, `"file_manager"`). For store services: set to `"http"` to indicate the HTTP API (section 4). |
| `manager` | string | Set to `"http"` for store services — PIC will call your container's HTTP API for account operations (see section 4). The reference services (`email`, `calendar`, `files`) use internal manager names (`"email_manager"`, `"calendar_manager"`, `"file_manager"`). |
| `credentials` | array of strings | Names of credential fields this service issues per peer. Most services use `["password"]`. The names appear in `peer_config_template` tokens. |
```json
@@ -188,7 +185,7 @@ Required when `has_accounts` is `true`.
### `compose`
For builtins set this to `null` — their containers come from the main `docker-compose.yml`. For store services this field is unused at the manifest level; compose configuration is provided via `compose-template.yml` in the package (see section 3).
Unused at the manifest level. Compose configuration is provided via `compose-template.yml` in the service package (see section 3). Set to `null` in the manifest.
### `backup`
@@ -221,12 +218,12 @@ Required when `has_egress` is `true`. Declares which outbound network interfaces
| `default` | string | The interface selected when the admin has not changed anything. |
| `allowed` | array of strings | The complete set of interfaces the admin can choose from. |
Valid interface identifiers: `cell_internet`, `openvpn`, `wireguard`, `tor`, `pic_cell`.
Valid interface identifiers: `default`, `wireguard_ext`, `openvpn`, `tor`.
```json
"egress": {
"default": "cell_internet",
"allowed": ["cell_internet", "openvpn", "wireguard", "tor", "pic_cell"]
"default": "default",
"allowed": ["default", "wireguard_ext", "openvpn", "tor"]
}
```
@@ -315,7 +312,7 @@ This section covers two related things: the `AccountManager` class that is PIC's
### How AccountManager works
`AccountManager` (`api/account_manager.py`) is the single entry point for all account operations across every service type. It is instantiated once in `api/managers.py` and holds references to the three builtin service managers (`email_manager`, `calendar_manager`, `file_manager`).
`AccountManager` (`api/account_manager.py`) is the single entry point for all account operations across every service type. It is instantiated once in `api/managers.py` and holds references to the service managers used by the reference services (`email_manager`, `calendar_manager`, `file_manager`).
When a peer account is provisioned, `AccountManager`:
@@ -463,9 +460,9 @@ The following PIC API endpoints are available for all services (builtins and sto
| `POST` | `/api/services/catalog/<id>/restart` | Restart the service containers. Builtins restart via the main compose stack; store services restart via their own compose project. |
| `POST` | `/api/services/catalog/<id>/reconfigure` | Re-render the compose file from the template and re-apply with `up -d` (rolling update). Store services only — builtins are reconfigured through their own settings routes. The request body must include a `compose_template` field containing the new template content. |
### Store service HTTP API (planned)
### Store service HTTP API
When `accounts.manager` is `"http"`, PIC will call your container's HTTP API for account operations. This path is not yet implemented in `AccountManager`; the dispatch table only covers `email_manager`, `calendar_manager`, and `file_manager`. The interface below is the planned contract — implement it now so your service is ready when HTTP dispatch is wired up.
When `accounts.manager` is `"http"`, PIC will call your container's HTTP API for account operations. **HTTP dispatch is not yet wired up in `AccountManager`** — the current dispatch table covers only `email_manager`, `calendar_manager`, and `file_manager` (used by the reference services). Implement this interface now so your service is ready when HTTP dispatch ships.
The base path is `/service-api/accounts` on your container's internal address. There is no authentication on this API — it is reachable only from within the `cell-network` Docker network.
@@ -551,13 +548,12 @@ The valid values for `egress.allowed` and what they mean:
| Value | Path |
|---|---|
| `cell_internet` | Default route through the cell's WAN interface (no VPN). |
| `default` | Default route through the cell's WAN interface (no VPN). |
| `wireguard_ext` | Traffic leaves through `wg_ext0` (fwmark `0x10`, table 110). |
| `openvpn` | Traffic leaves through `tun0` (fwmark `0x20`, table 120). |
| `wireguard` | Traffic leaves through a second WireGuard interface `wg_ext0` (fwmark `0x10`, table 110). |
| `tor` | Traffic is redirected to the Tor transparent proxy on port 9040 (fwmark `0x30`, table 130). |
| `pic_cell` | Traffic routes through a connected peer cell via a site-to-site WireGuard link. |
List only the interfaces that make sense for your service in `allowed`. Listing all five is fine if your service is general-purpose. The `default` value is used when the admin has not changed anything. Always include `cell_internet` in `allowed` so the admin has a way to use the normal path.
List only the interfaces that make sense for your service in `allowed`. The `default` value is used when the admin has not changed anything. Always include `default` in `allowed` so the admin has a way to use the normal path.
The egress field in the manifest tells PIC what options to present in the UI. Actual enforcement requires the cell to have the corresponding exit type configured (an OpenVPN config uploaded, a WireGuard external config active, etc.). If the chosen exit is not active, packets will be dropped by the kill-switch FORWARD rule in `cell-wireguard`.
@@ -635,7 +631,21 @@ The result is that any WireGuard peer can reach `https://home.alice.pic.ngo/` im
---
## 8. Submitting to the store
## 8. Reference implementations
The `email`, `calendar`, and `files` services in `pic-services/services/` are the canonical examples of a complete store service. They demonstrate the full feature set:
| Service | Notable features demonstrated |
|---|---|
| `email` | `has_accounts`, `has_egress`, multi-container (`cell-mail` + `cell-rainloop`), `extra_backends`, custom image baking defaults via Dockerfile |
| `calendar` | `has_accounts`, CalDAV `peer_config_template`, htpasswd account provisioning |
| `files` | `has_accounts`, `has_storage`, WebDAV + Filegator `extra_backends`, `backup.volumes` with multiple entries |
When in doubt about how to structure your manifest or compose template, use these as the reference.
---
## 9. Submitting to the store
### Package format
@@ -700,32 +710,32 @@ Increment `version` in `manifest.json` with every change you submit. PIC does no
## Appendix: manifest field quick reference
| Field | Builtin | Store | Notes |
|---|---|---|---|
| `schema_version` | required | required | Must be `3` |
| `id` | required | required | |
| `name` | required | required | |
| `description` | required | required | |
| `version` | required | required | |
| `author` | required | required | |
| `kind` | required | required | `"builtin"` or `"store"` |
| `min_pic_version` | optional | optional | |
| `capabilities.*` | required | required | All six flags must be present |
| `subdomain` | if `has_subdomain` | if `has_subdomain` | |
| `extra_subdomains` | optional | optional | |
| `backend` | if `has_subdomain` | if `has_subdomain` | |
| `extra_backends` | optional | optional | |
| `containers` | optional | optional | Informational |
| `config_schema` | if `has_admin_config` | if `has_admin_config` | |
| `peer_config_template` | if `has_accounts` | if `has_accounts` | |
| `accounts` | if `has_accounts` | if `has_accounts` | |
| `compose` | `null` | `null` | |
| `backup` | if `has_storage` | if `has_storage` | |
| `egress` | if `has_egress` | if `has_egress` | |
| `storage` | if `has_storage` | if `has_storage` | |
| `image` | not used | required | |
| `container_name` | not used | required | |
| `volumes` | not used | optional | |
| `env` | not used | optional | |
| `iptables_rules` | not used | optional | |
| `caddy_route` | not used | optional | |
| Field | Required | Notes |
|---|---|---|
| `schema_version` | yes | Must be `3` |
| `id` | yes | |
| `name` | yes | |
| `description` | yes | |
| `version` | yes | |
| `author` | yes | |
| `kind` | yes | Must be `"store"` |
| `min_pic_version` | no | |
| `capabilities.*` | yes | All six flags must be present |
| `subdomain` | if `has_subdomain` | |
| `extra_subdomains` | no | |
| `backend` | if `has_subdomain` | |
| `extra_backends` | no | |
| `containers` | no | Informational |
| `config_schema` | if `has_admin_config` | |
| `peer_config_template` | if `has_accounts` | |
| `accounts` | if `has_accounts` | |
| `compose` | no | Always `null` — compose config goes in `compose-template.yml` |
| `backup` | if `has_storage` | |
| `egress` | if `has_egress` | |
| `storage` | if `has_storage` | |
| `image` | yes | Must match `git.pic.ngo/roof/*` |
| `container_name` | yes | Must match `^cell-[a-z0-9][a-z0-9-]{0,30}$` |
| `volumes` | no | |
| `env` | no | |
| `iptables_rules` | no | |
| `caddy_route` | no | |