Dev – Build a Store Service
Dmitrii Iurco edited this page 2026-06-11 15:47:54 -04:00

Status: Active | Owner: @roof | Applies to: main (2026-06) | Updated: 2026-06-11

Dev – Build a Store Service

This page is a narrative walkthrough and submission guide. The complete field-by-field reference is Dev – Service Manifest Reference.


What a store service is

A store service is a Docker container (or set of containers) described by a single JSON manifest file. PIC fetches the manifest when the admin clicks Install, validates it, renders a Docker Compose file, starts the container(s), and wires up DNS and Caddy routes automatically.

The email, calendar, and files services in pic-services/services/ are the reference implementations for the full feature set.


Manifest schema (v3)

Every manifest must have "schema_version": 3 and "kind": "store". Required identity fields:

{
  "schema_version": 3,
  "id": "notes",
  "name": "Notes",
  "description": "Self-hosted Markdown notes",
  "version": "1.0.0",
  "author": "your-name",
  "kind": "store",
  "image": "git.pic.ngo/roof/pic-notes:latest",
  "container_name": "cell-notes"
}

The image field must match git.pic.ngo/roof/*. External registries are rejected. The container_name must match ^cell-[a-z0-9][a-z0-9-]{0,30}$.

Capabilities

Six boolean flags tell PIC which integrations to activate:

Flag What it enables
has_subdomain A Caddy reverse-proxy route and a DNS record
has_accounts Per-peer account provisioning
has_admin_config A settings form rendered from config_schema
has_storage Backup integration
has_egress Per-service outbound interface selection
has_api_hooks Reserved; set false

Subdomain and routing

When has_subdomain is true, provide:

  • subdomain — the primary subdomain (for example, "notes"). Results in notes.<cell-domain>.
  • backend — the container:port Caddy proxies to (for example, "cell-notes:8080").
  • extra_subdomains / extra_backends — additional subdomains pointing to different backends (the files service uses this for WebDAV vs Filegator).

Reserved subdomains that cannot be used: api, webui, admin, www, ns1, ns2, git, registry, install.

Volumes and environment

"volumes": [
  {"name": "notes-data", "mount": "/data/notes"}
],
"env": [
  {"key": "NOTES_SECRET", "value": "${PIC_SECRET_NOTES_SECRET}"}
]

Volume mounts to system paths (/, /etc, /var, /proc, /sys, /dev, /app, /run, /boot) and to paths that overlap the PIC project root are rejected at install time.

iptables rules

To let VPN peers reach your container's port:

"iptables_rules": [
  {
    "type": "ACCEPT",
    "dest_ip": "${SERVICE_IP}",
    "dest_port": 8080,
    "proto": "tcp"
  }
]

Only ACCEPT type rules are permitted. ${SERVICE_IP} is replaced with the allocated container IP at install time.

Backup

When has_storage is true:

"backup": {
  "volumes": [
    {"container": "cell-notes", "path": "/data/notes", "name": "notes_data"}
  ],
  "config_paths": ["config/notes"]
}

Each volumes entry is archived via docker exec <container> tar and stored as <name>.tar.gz in the backup. config_paths are host paths copied directly.

Account provisioning

When has_accounts is true:

"accounts": {
  "manager": "http",
  "credentials": ["password"]
},
"peer_config_template": {
  "url": "https://notes.{domain}/",
  "username": "{peer.username}",
  "password": "{peer.service_credentials.notes.password}"
}

For "manager": "http", your container must implement the account management HTTP API at /service-api/accounts:

  • POST /service-api/accounts — create account ({"username": "...", "password": "..."})
  • DELETE /service-api/accounts/{username} — delete account
  • GET /service-api/accounts — list usernames

The HTTP dispatch for the "http" manager is implemented in AccountManager (api/account_manager.py) via _provision_http / _deprovision_http. The reference services (email/calendar/files) use named internal managers; set manager: "http" to own accounts in your own container via the endpoints above.

Egress

When has_egress is true:

"egress": {
  "default": "default",
  "allowed": ["default", "wireguard_ext", "openvpn", "tor", "sshuttle", "proxy"]
}

Always include "default" in allowed.


Compose template

Include a compose-template.yml alongside your manifest.json. ServiceComposer substitutes these variables before writing the per-service docker-compose.yml:

Variable Value
${PIC_DOMAIN} Cell's effective domain
${PIC_CELL_NAME} Short cell name
${PIC_DATA_DIR} Absolute path to PIC data directory
${PIC_SERVICE_ID} Service id from the manifest
${PIC_CFG_<KEY>} Admin-configured value for config_schema field <key> (uppercase)
${PIC_SECRET_<NAME>} Auto-generated random secret (secrets.token_urlsafe(24)); generated once, reused on reconfigure
${SERVICE_IP} Container IP allocated from 172.20.0.20172.20.0.254

Use named Docker volumes for service data rather than bind mounts to avoid host-path resolution issues.


Submitting to the store

  1. Fork https://git.pic.ngo/roof/pic-services
  2. Create services/<your-id>/manifest.json
  3. Add compose-template.yml
  4. Add an entry to index.json
  5. Open a pull request against main

The review checks:

  • Image is at git.pic.ngo/roof/*
  • No volume mounts to system or PIC-root paths
  • iptables_rules are ACCEPT only
  • subdomain does not collide with reserved names or existing services
  • install.sh (if included) does not install system packages or run curl | bash

Increment version in manifest.json with every submission. PIC does not auto-update installed services.


Signing and verification

Merged services are built by the two-stage pipeline (untrusted kaniko build → trivy scan → cosign sign → push), and the manifest's image is rewritten to a @sha256: digest. Cells verify the signature with cosign before starting any store container. The default image_verification mode is enforce — an unsigned or undigested image will not install.

While developing locally, set image_verification.mode to warn or off in config/api/cell_config.json (then restart the API container) to run images that have not been through the pipeline yet.

See ADR – 001 Store Images Are Signed and Verified by Cells for the rationale behind signing and verification.

See Dev – Service Manifest Reference for the complete field reference, compose template variables, the account-provisioning HTTP interface, and backup/egress integration details.