A Package is PMA's unit of functionality. Every service lives in
packages/<svc>/as a self-contained unit. The Framework knows nothing service-specific — it reads each Package's manifest and dispatches generic operations.
This is Golden Rule 12 codified: service logic ONLY in packages/.
packages/<svc>/
├── manifest.yaml # SSOT — service identity, ports, DB, SSO, backup, health, E2E
├── docker-compose.yaml # Container definitions
├── net.manifest.yaml # asd-owned route declaration (Caddy config)
├── tpl.env # env-var template (defaults, credential placeholders)
├── install.just # service-specific just recipes
└── scripts/ # lifecycle hooks
├── pre_start_init.ts # runs before container start
├── post_start_init.ts # runs after container healthy
├── post_data_restore.sh # runs after a restore
└── ensure-config.ts # idempotent config-apply
Plus optional:
dashboards/ — Grafana JSON dashboards (for the grafana package)workflows/ — n8n workflow JSON (for the n8n package)blueprints/ — Authentik blueprints (for the authentik package)Note: the Framework calls into the Package via well-defined hook contracts.
The Package never calls back into the Framework directly — it can only
declare (manifest) or do (scripts).
manifest.yamlThe single source of truth for everything about the service. Full reference:
/pma/reference/manifest.
The Framework reads this file to know:
containers[])requirements.ports, port_env)health.endpoint, health.expected_code)backup.type, backup.volumes, backup.database)fixes.post_restore.functions)sso.type, sso.redirect_path)framework.profiles)docker-compose.yamlStandard Compose file. Naming follows project conventions:
${CONTAINER_PREFIX}${SERVICE}[-<role>]asd-${ASD_ENV} (shared across all services in this env)volumes:)net.manifest.yamlasd-owned route declaration. PMA declares; asd net apply activates.
id: ${{ macro.concat('env:ENV_PREFIX', 'redmine') }}
endpoint:
url: http://127.0.0.1:${{ env.REDMINE_PORT }}
caddy:
hostRoute:
host: ${{ macro.concat('env:ENV_PREFIX', 'redmine') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}
Full schema: /asd/reference/net-manifest.
tpl.envPer-package env-var template. Bootstrap walks every active package's
tpl.env and populates .env on first install.
REDMINE_PORT=8083
REDMINE_IMAGE_TAG=5-alpine
REDMINE_DB_PASSWORD={{random:32}}
REDMINE_ADMIN_PASSWORD={{random:24}}
Pattern placeholders ({{random:32}}, {{service-url}}) are expanded
during bootstrap.
install.justService-specific just recipes. Inherits framework helpers; can override.
# packages/redmine/install.just
import '../common/install.common.just'
redmine-shell:
docker exec -it {{CONTAINER_PREFIX}}redmine bash
redmine-psql *args:
docker exec -it {{CONTAINER_PREFIX}}redmine-postgres psql -U redmine {{args}}
The framework's Justfile imports every active package's install.just,
so per-service recipes appear as <svc>-<verb> in just --list.
Hooks run at well-defined points in the package lifecycle. The Framework
invokes them; they don't invoke each other.
| Hook | When | Purpose |
|---|---|---|
pre_start_init.ts |
Before docker compose up |
DB schema migrations, secret generation, file permissions |
post_start_init.ts |
After container healthy | API-driven config (e.g. Wiki.js theming, n8n workflow import) |
post_data_restore.sh |
After backup restore | Per-service post-restore fixes (unlock migration tables, etc.) |
ensure-config.ts |
Idempotent — called multiple times | Reapply config that drifts (Mermaid injection, OAuth callbacks) |
The ensure-config.ts pattern is what makes config self-healing — call it
on every restart and the config converges to the declared state.
/install-service <svc> (the AI-agent skill) or use the/pma/cookbook/install-a-new-service recipe.packages/<svc>/ with the five required files.just compose-generate — regenerates docker/docker-compose.yml.just contract-validate — confirms manifest matches schema.just package install <svc> — bootstrap installs the service.The Framework picks the new service up automatically — no edits to
scripts/, Justfile, or any other framework code.
Per Golden Rule 12, the Rule Engine blocks:
if service == "redmine" (or any service name) in framework codepackages/The framework iterates over the active profile dynamically:
// ✓ Correct
for (const svc of getActiveServices()) {
const manifest = readManifest(svc);
if (manifest.sso?.type === 'oauth') {
setupOAuth(svc, manifest);
}
}
// ✗ Blocked
if (service === 'redmine') {
setupRedmineOAuth();
}
/pma/reference/manifest — full manifest schema./pma/internals/bootstrap — how bootstrap walks active packages./pma/internals/install-skill — the AI-agent protocol for adding services./pma/cookbook/install-a-new-service — the operator-side recipe.