Status: Active | Owner: @roof | Updated: 2026-06-11
ADR – 003 All Optional Functionality Ships as Store Services
Context
The initial PIC implementation treated email, calendar/contacts, and file storage as built-in components. Their manager classes (EmailManager, CalendarManager, FileManager) lived directly in api/, their containers were defined in docker-compose.yml alongside the core stack, and their API routes were always registered regardless of whether the services were wanted.
This design created several problems:
- Bloated core. Every PIC install ran email, calendar, and file containers even when the admin had no use for them. This consumed RAM, disk, and attack surface for no benefit.
- Coupled release cycles. A bug in the email service required a PIC core release. An improvement to file storage blocked on a full core release cycle.
- No uniform lifecycle. Installing email was "enable in config and restart"; installing a new service later was a different process. There was no single install/uninstall/backup abstraction.
- Growing
docker-compose.yml. Every new service added to the monolith made the compose file larger and harder to reason about.
Options Considered
Option A — Keep services built-in, add feature flags
Add configuration flags (email_enabled, calendar_enabled, etc.) so admins can disable services they do not want. Containers are still defined in docker-compose.yml but only started if enabled.
Rejected because: the compose file and the core API still carry all the service code. The attack surface reduction is superficial — the code paths are present even when disabled. Coupling release cycles remains a problem.
Option B — Git submodules for each service
Move each service to its own repository as a git submodule. The core docker-compose.yml includes service compose files from submodules.
Rejected because: git submodule workflows are operationally complex for self-hosted users. Updating a service requires updating the submodule reference in the core repo, which re-couples release cycles. Backup and account provisioning still need to be wired into the core.
Option C — Store services with a manifest-driven lifecycle (chosen)
Define a manifest schema (schema_version: 3, "kind": "store") that encodes everything the core needs to know about a service: where to route HTTPS, what accounts to provision, what to back up, what egress options to expose. The core service store installs any service from the catalog at runtime without a core code change or restart.
The core shrinks to exactly six containers (Caddy, CoreDNS, NTP, WireGuard, Flask API, React WebUI). Everything else — email, calendar, files, webmail, connectivity exits — ships as a store service.
Decision
All optional functionality ships as a store service (Option C). The six core containers are the stable, security-critical surface; everything else installs and uninstalls independently via the service store.
The ServiceRegistry (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.
Account provisioning for store services uses the HTTP dispatch path in AccountManager (api/account_manager.py) via _provision_http / _deprovision_http, which calls the service container's /service-api/accounts endpoint. The reference services (email, calendar, files) use named internal managers for backward compatibility; new store services use "manager": "http" in their manifest.
Consequences
- Uniform install/uninstall/backup. Every service follows the same lifecycle: install from the store, run via its own compose project under
data/services/<id>/, back up via manifest-declared volume paths, uninstall with data preserved on disk. No special cases. - Smaller attack surface for minimal installs. A cell that only needs VPN and DNS runs six containers. Email, calendar, and files add containers only when explicitly installed.
- Services upgrade independently. Bumping the email service version is a manifest version bump and a store re-install, not a PIC core release.
- Core manager classes for email/calendar/files remain (
EmailManager,CalendarManager,FileManager), but they are invoked throughServiceRegistryrather than being always-active. They will eventually be moved into the service packages themselves as the migration matures. - The manifest schema (
schema_version: 3) is a stability contract. Breaking changes require a new schema version and a migration path. The six capability flags (has_subdomain,has_accounts,has_admin_config,has_storage,has_egress,has_api_hooks) are the extension points; adding a new integration type requires a new flag. - Community services must pass the signing pipeline before cells will install them (under the default
enforceimage verification mode). See ADR – 001 Isolated Build and Signing Pipeline for Store Images.
Internals: see Dev – Build a Store Service and Dev – Service Manifest Reference.
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