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 innotes.<cell-domain>.backend— thecontainer:portCaddy 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 accountGET /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.20–172.20.0.254 |
Use named Docker volumes for service data rather than bind mounts to avoid host-path resolution issues.
Submitting to the store
- Fork
https://git.pic.ngo/roof/pic-services - Create
services/<your-id>/manifest.json - Add
compose-template.yml - Add an entry to
index.json - 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_rulesareACCEPTonlysubdomaindoes not collide with reserved names or existing servicesinstall.sh(if included) does not install system packages or runcurl | 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.
Personal Internet Cell
New here?
Users
User – Connect to the VPN User – Use Your Services User – Troubleshooting
Admins
Admin – Overview Admin – Install and First Run Admin – Configure Domains and TLS Admin – Manage Services Admin – Configure Connectivity Admin – Manage Peers Admin – Back Up and Restore Admin – Logging and Audit Admin – Monitor and Troubleshoot
Developers
Dev – Overview Dev – Architecture Dev – Build a Store Service Dev – Service Manifest Reference Dev – API Reference Dev – Testing Dev – Install Internals
Decisions (ADRs)
ADR – 001 Store Images Are Signed and Verified by Cells ADR – 002 Named Connection Instances for Connectivity ADR – 003 All Optional Functionality Ships as Store Services
Meta
Meta – Glossary Meta – Template Runbook Meta – Template ADR
Archive
Archive – User Guide Archive – ADR 004 The Wiki Is the Single Documentation Source