| name | external-app-create |
| description | Scaffold a new Cozystack external app package inside an external-apps repository. Generates the full chart skeleton (Chart.yaml, Makefile, values.yaml with cozyvalues-gen annotations, templates), registers it in core/platform (namespace, HelmRepository, HelmChart, HelmRelease, ApplicationDefinition), and wires dependency integration — supports managed CNPG Postgres clusters provisioned in-chart and external secret references for pre-existing services. Use when adding a new application (e.g. Immich, Gitea, Nextcloud) to an external-apps repo that follows the cozystack/external-apps-example layout. |
| argument-hint | <app-name> [--depends-on=postgres,redis] [--operator=<chart-repo-url>] [--repo-dir=<path>] |
cozystack:external-app-create
This skill scaffolds a new Cozystack external app package. It creates all files needed for the app to appear in the Cozystack dashboard and be deployable via the GitOps pipeline (GitRepository → Flux HelmRelease → ApplicationDefinition).
This is a generate-only skill. It never applies anything to a cluster, never commits, and never pushes. The user handles git operations themselves.
Work in reasoning mode. Follow the phases in order. When a step fails or is ambiguous, stop and ask — do not guess API shapes or secret names.
Use the phrasing "cozystack:external-app-create" (not "the skill") in messages to the user, and state progress at each phase boundary.
Match the operator's natural language detected from prior conversation messages — use it in prompts, AskUserQuestion options, summaries, and gates. Generated Helm template files, ApplicationDefinition values, and any text destined for git or the cozystack dashboard stay in their canonical form (usually English) per cozystack's public-content rules.
Phase 1 — Parse arguments
$ARGUMENTS contains the free-form tail after /cozystack:external-app-create. Extract:
- Positional
<app-name> — lowercase, hyphen-separated (e.g., immich, my-app). Required.
--depends-on=<list> — comma-separated dependency names (e.g., postgres, redis). Default: none.
--operator=<url> — Helm chart repository URL for a required operator (e.g., https://immich-app.github.io/immich-charts). Default: none.
--repo-dir=<path> — path to the external-apps repository root. Default: current working directory.
If <app-name> is missing, use AskUserQuestion to ask for it.
Phase 2 — Pre-flight checks
Bail early if any check fails.
-
Repository structure: verify $REPO_DIR contains init.yaml, packages/core/platform/Chart.yaml, scripts/package.mk, and the five platform template files packages/core/platform/templates/{namespaces,helmrepositories,helmreleases,helmcharts,cozyrds}.yaml. If any are missing, tell the user to cd into the external-apps repo root or pass --repo-dir — Phase 8 appends to all five.
-
Tools installed: check that yq (v4), jq, base64, helm, and cozyvalues-gen are available via command -v. If cozyvalues-gen is missing, print:
cozyvalues-gen is required. Install it from:
https://github.com/cozystack/cozyvalues-gen/releases/latest
Do not install it automatically. Stop.
-
No collision: verify packages/apps/$APP_NAME/ does not already exist. If it does, stop and ask the user whether to overwrite or pick a different name.
-
Cozystack contract resolution source: Phase 4 Step 2 needs to read packages/system/<dep>-rd/cozyrds/<dep>.yaml from cozystack. Detect the best available source in this order and record it as $COZYSTACK_CONTRACT_SOURCE:
local — $COZYSTACK_REPO is set and points at a cozystack checkout containing packages/system/. Fastest, works offline.
github — gh CLI is authenticated (gh auth status succeeds). Resolves against cozystack/cozystack@main.
cluster — kubectl config current-context succeeds and the target context is a cozystack cluster. Authoritative for that cluster version; read-only.
If none is available, warn the user. Phase 4 Pattern C will be unavailable; only Patterns A and B remain.
Phase 3 — Gather app specification
Use AskUserQuestion to collect:
- Chart source: is there a maintained first-party Helm chart for this app (official repo, community-run, appeared on Artifact Hub)?
- Default: wrap the upstream chart via Flux
HelmRelease. You inherit upgrades, init-jobs, probes, PDBs, ingress templates, and every breaking-change mitigation the upstream maintainers ship. Phase 5 will register its HelmRepository; Phase 7 emits the wrapping HelmRelease.
- Fall back to custom templates only when no upstream chart exists, the upstream is abandoned, or it conflicts with Cozystack conventions in ways that cannot be overridden via values. Custom templates shift lifecycle ownership onto the skill's user — every upstream CVE must then be tracked by hand.
- Record
$CHART_SOURCE as upstream or custom. If upstream, collect repo URL + chart name + version here (feeds Phase 5).
- Container image: image reference (e.g.,
ghcr.io/immich-app/immich-server:v1.120.0).
- Public port: does the app expose an HTTP port? If yes, which port number? Should an Ingress template be generated?
- Persistent storage: does the app need a PVC? If yes, default size (e.g.,
10Gi).
- Icon: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a
logos/ placeholder and Phase 8's base64 < logos/$APP_NAME.svg step will fail until the user provides one.
- Dashboard metadata: Display Name singular (e.g.,
Immich), Display Name plural (e.g., Immichs — do not infer automatically; apps like MinIO, Nextcloud, or Gitea read incorrectly when the singular is reused), Description (e.g., Self-hosted photo and video management solution), Category (e.g., Media), and Tags (comma-separated list, e.g., photo, video).
- Resource definition: Kind (e.g.,
Immich) and Plural (e.g., immichs) for the ApplicationDefinition created in Phase 8.
Record all answers. Proceed only after user confirms the summary.
Phase 4 — Resolve and gather dependencies
Dependencies are discovered from the app itself, not asked blind. The phase runs in five steps: discover what the app needs, resolve each against a cozystack contract, pick the integration pattern, collect spec values, record wiring. Never skip Steps 1–2 — they are what prevent wrong or invented Secret/Service references.
Step 1 — Chart requirement analysis
Discover candidate dependencies from the chart itself.
When $CHART_SOURCE = upstream: pull the upstream chart metadata once and inspect it.
helm repo add --force-update $SOURCE_REPO_NAME $SOURCE_REPO_URL
helm show chart $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/Chart.yaml
helm show values $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/values.yaml
helm show readme $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/README.md
For OCI sources use oci://$SOURCE_REPO_URL/$SOURCE_CHART_NAME directly — helm show handles OCI without repo add.
Extract three signals:
- Declared subcharts from
Chart.yaml → dependencies[]. Match names from this vocabulary: postgresql, postgresql-ha, mariadb, mysql, redis, redis-cluster, valkey, mongodb, kafka, clickhouse, rabbitmq, memcached, nats, minio. Every matched subchart is a candidate dep — note its enabled default and which values key disables it (typically <subchart-alias>.enabled: false).
- Config paths in
values.yaml. Recurse and record every path matching these keywords until you reach a leaf with host, hostname, url, dsn, uri, password, user, username, database, dbname, port: database, db, postgres*, mysql, mariadb, cache, redis, valkey, session, queue, broker, mongodb, kafka, rabbitmq, memcached. These paths are the targetPath values the app chart will later inject via valuesFrom.
- README hints — maintainers usually call out supported databases ("supports PostgreSQL, MySQL, SQLite") near the top and show sample values for external services. Treat this as narrative context, not definitive — the actual schema in
values.yaml wins on conflict.
Emit findings in a table:
| Dependency | Signals | Wiring path(s) | Subchart to disable |
|---|
| postgres | Chart dep postgresql-ha; values keys gitea.config.database.* | gitea.config.database.HOST, …USER, …NAME, …PASSWD | postgresql-ha.enabled: false |
| redis | Values keys gitea.config.cache.*, gitea.config.session.* | gitea.config.cache.HOST, …PASSWORD | redis-cluster.enabled: false |
Present the table to the user via AskUserQuestion: "Detected these dependencies from the chart. Add, remove, or mark any as optional?" This is where heuristics can be corrected — the user might know the app can use one of several databases, or that a detected cache is optional.
When $CHART_SOURCE = custom: there is no chart to introspect. Ask the user directly which backing services the app needs and, for each, which config file / env variable the app reads.
Step 2 — Contract resolution (per dependency)
For each dependency from Step 1, resolve a $DEP_CONTRACT from cozystack. Try these sources in order; stop at the first success:
-
Local cozystack checkout — when $COZYSTACK_REPO is set. Read:
cat $COZYSTACK_REPO/packages/system/<dep>-rd/cozyrds/<dep>.yaml
-
GitHub API — when gh CLI is available. Fetch directly from upstream cozystack:
gh api repos/cozystack/cozystack/contents/packages/system/<dep>-rd/cozyrds/<dep>.yaml \
--jq .content | base64 --decode
-
Live cluster — when kubectl has a usable context (kubectl config current-context succeeds) AND the dep's ApplicationDefinition is installed:
kubectl --context $CTX get applicationdefinition <dep> --output yaml
Confirm the current context is the intended cozystack cluster before relying on this source (read-only operation, but still worth double-checking). This source is authoritative for that specific cluster version — if sources 1 or 2 disagree, prefer the live source and note the drift to the user.
From the resolved document, extract and record $DEP_CONTRACT.<field>:
| Contract field | Source path in the ApplicationDefinition |
|---|
kind | spec.application.kind (Postgres, Redis, MariaDB, …) |
plural | spec.application.plural |
prefix | spec.release.prefix (postgres-, redis-, …) |
secretTemplates | spec.secrets.include[].resourceNames (list of Go-template strings using {{ .name }}) |
serviceTemplates | spec.services.include[].resourceNames |
specSchema | spec.application.openAPISchema (JSON-parseable; drives Step 4) |
apiVersion | apiVersion of the ApplicationDefinition itself (typically cozystack.io/v1alpha1) |
If all three sources fail, record Pattern C as unavailable for this dependency. The user must install the ApplicationDefinition in the target cluster, provide $COZYSTACK_REPO, or fall back to Pattern A or B.
Step 3 — Integration pattern choice
For each dependency with a resolved $DEP_CONTRACT, offer the integration pattern via AskUserQuestion. Default is Pattern C; Pattern A and Pattern B are opt-ins. Pattern C is unavailable (greyed out) when Step 2 failed.
- Pattern C — Sibling cozystack ApplicationDefinition (recommended for external apps). The app chart emits a
${DEP_CONTRACT.kind} CR; cozystack reconciles it into its own HelmRelease; the app consumes the resulting Secret and Service. See the Pattern C subsection below.
- Pattern A — In-chart operator CR (system-style escape hatch). The app chart creates the operator CR directly (CNPG
Cluster, Spotahome RedisFailover, etc.). Use when no cozystack ApplicationDefinition exists for the dep or when the app is explicitly system-scoped (harbor/keycloak style). See the Pattern A subsection.
- Pattern B — External reference. The user provides connection details via values; the app chart provisions nothing. See the Pattern B subsection.
Pattern C — Sibling cozystack ApplicationDefinition
The app chart creates a ${DEP_CONTRACT.kind} CR (e.g., Postgres, Redis) inside its own templates. The cozystack controller reconciles that CR into a HelmRelease named ${DEP_CONTRACT.prefix}<cr-name>, deploying the corresponding packages/apps/<dep>/ chart — the same chart a tenant invokes through the dashboard.
Why this is the default for external apps:
- The sibling instance shows up in the dashboard as a first-class entity. A tenant can list, inspect, back up, and restore it independently of the wrapping app.
- WorkloadMonitor, PodMonitor, backup schedules, and migrations shipped by cozystack's own
apps/<dep>/ chart apply automatically — none of that needs to be re-implemented per app.
- Upgrading cozystack upgrades the dependency wiring for every consumer at once.
All wiring is driven by $DEP_CONTRACT from Step 2 — never hardcode prefixes, secret names, or service names. Phase 7 emits one Pattern C template per dep (templates/<dep>.yaml) and wires the main workload HelmRelease's values / valuesFrom against $DEP_CONTRACT.secretTemplates and $DEP_CONTRACT.serviceTemplates substituted with the CR's own name.
Pattern A — In-chart operator CR (system-style)
The app chart creates the operator CR itself (e.g., CNPG Cluster, Spotahome RedisFailover) and owns both the CR and its output Secret. No separate dashboard entity for the dep. Use when a cozystack ApplicationDefinition for the dep is unavailable, or when the app is explicitly system-scoped (harbor, keycloak). For tenant-facing external apps, prefer Pattern C.
Pattern A still requires research: the operator CR shape and its Secret/Service output convention are operator-specific. The Pattern A catalog appendix records the verified shapes for CNPG and Spotahome. For any operator not in that catalog, follow the research procedure in the appendix before writing Phase 7 templates.
Pattern B — External reference
The app expects a pre-existing service. The user provisions it separately and passes connection details as values — typically postgres.host, postgres.port, postgres.secretName. The app chart does not provision anything. Collect which values.yaml fields to expose and how the app consumes them (env vars, config mount).
Step 4 — Collect spec values
Drive field selection from $DEP_CONTRACT.specSchema (Pattern C) or from the operator CR schema researched for Pattern A — not a hand-picked list. For Pattern C postgres, specSchema declares size, replicas, users, databases, external, storageClass, etc.; collect defaults for each the user expects to expose. For Pattern B, the values schema is whatever the user and the app agree on.
Step 5 — Record wiring mapping
The shape of the wiring record depends on chart source:
-
Upstream chart ($CHART_SOURCE = upstream): record a list of {targetPath, source} entries. targetPath is one of the value paths surfaced in Step 1; source is either an inline value (e.g., the Service hostname) or a valuesFrom reference using $DEP_CONTRACT.secretTemplates + the CR's name.
Example for Gitea + Pattern C postgres (CR named {{ .Release.Name }}-db, contract prefix postgres-, secret template postgres-{{ .name }}-credentials):
values:
gitea:
config:
database:
DB_TYPE: postgres
HOST: postgres-{{ .Release.Name }}-db-rw:5432
NAME: {{ .Values.database.name }}
USER: {{ .Values.database.user }}
valuesFrom:
- kind: Secret
name: postgres-{{ .Release.Name }}-db-credentials
valuesKey: {{ .Values.database.user }}
targetPath: gitea.config.database.PASSWD
-
Custom chart ($CHART_SOURCE = custom): record container env entries. valueFrom.secretKeyRef targets $DEP_CONTRACT.secretTemplates (Pattern C) or the operator's output Secret (Pattern A). Inline values come from services or helm expressions.
Present a single summary table of all dependencies with: name, chosen pattern, resolved kind, resolved secret/service, wiring targets. Proceed only after user confirms.
Phase 5 — Register upstream Helm chart sources (conditional)
Flux reconciles external Helm charts via the HelmRepository resource. Two situations require a HelmRepository registration in this phase:
- App wraps an upstream Helm chart (see Phase 3 question 1). Example: Gitea wraps
https://dl.gitea.com/charts.
- App requires a dedicated operator shipped as a separate chart. Example:
minecraft-operator from oci://ghcr.io/lexfrei/charts.
Skip this phase only if BOTH conditions are false (app uses custom templates AND no dedicated operator).
For each source needed, use AskUserQuestion to collect:
$SOURCE_ROLE — main (upstream chart for the app itself) or operator (dedicated operator chart).
$SOURCE_REPO_URL — repository URL. Prefix with oci:// for OCI registries, otherwise use plain HTTPS.
$SOURCE_REPO_TYPE — oci if the URL starts with oci://, otherwise https. Phase 8 selects the HelmRepository variant by this exact value.
$SOURCE_CHART_NAME — chart name inside the repository (e.g., gitea, minecraft-operator, immich).
$SOURCE_CHART_VERSION — pinned version or semver range (e.g., 12.0.1, >=1.0.0). Avoid '*' in production use.
$SOURCE_REPO_NAME — alias used as HelmRepository.metadata.name. Default: $APP_NAME for main, $APP_NAME-operator for operator.
$SOURCE_NAMESPACE — namespace the HelmRepository lives in. Default: external-$APP_NAME-operator for operator sources, external-$APP_NAME for main sources (the same namespace later hosts any app-scoped HelmRelease).
No files are created in this phase. All source and release resources are written in Phase 8 alongside the other platform resources.
Phase 5.5 — Present plan
Before any file is written, assemble every decision gathered so far into a single plan and present it to the user once. This is the gate — after approval, Phases 6–8 proceed without further prompts (except critical ones, e.g., existing-file collision). Without this consolidated view, the user never sees the whole picture until files start appearing on disk.
Build a plan document with five sections:
- App summary —
$APP_NAME, Kind, Plural, dashboard metadata (Display Name, Description, Category, Tags), icon status (supplied / placeholder).
- Chart source —
$CHART_SOURCE (upstream or custom). For upstream, include $SOURCE_REPO_URL, chart name, version. For custom, note "no upstream wrapper, templates authored from scratch".
- Contract source — which of
local / github / cluster was used in Phase 4 Step 2 ($COZYSTACK_CONTRACT_SOURCE). Flag if cozystack main was used vs. a pinned version.
- Files to create / modify — explicit path list, grouped:
packages/apps/$APP_NAME/Chart.yaml, Makefile, values.yaml, values.schema.json, README.md, .helmignore, logos/$APP_NAME.svg.
packages/apps/$APP_NAME/templates/<dep>.yaml per Pattern C/A dependency (e.g., postgres.yaml, redis.yaml).
packages/apps/$APP_NAME/templates/$APP_NAME.yaml (HelmRelease wrapper or Deployment).
packages/core/platform/templates/{namespaces,helmrepositories,helmreleases,helmcharts,cozyrds}.yaml — only the ones that actually get modified, with the specific entry each receives.
- Dependency wiring table — for every dep, one row: chosen pattern, resolved kind, CR name template, output Secret template + key, output Service + port, upstream chart
targetPath (for HelmRelease wrapper) or container env name (for custom chart). This is the table from Phase 4 Step 5, restated for the approval gate.
Also list any open items explicitly: missing icon, missing upstream chart version, skipped dependency pattern, unresolved $DEP_CONTRACT. The user should not be surprised later.
Example for /cozystack:external-app-create gitea:
App : gitea (Kind: Gitea, Plural: giteas)
Dashboard : "Gitea" / "Self-hosted Git service" / Developer tools / git, vcs
Chart source : upstream — https://dl.gitea.com/charts gitea@12.0.1 (HelmRelease wrapper)
Contract source : local ($COZYSTACK_REPO=/Users/kitsunoff/git/github.com/cozystack/cozystack)
Create (packages/apps/gitea/):
Chart.yaml, Makefile, values.yaml, values.schema.json, README.md, .helmignore
logos/gitea.svg ⚠ placeholder — supply real SVG before make generate
templates/postgres.yaml Pattern C → apps.cozystack.io/v1alpha1 Postgres
templates/redis.yaml Pattern C → apps.cozystack.io/v1alpha1 Redis
templates/gitea.yaml HelmRelease wrapping gitea/gitea:12.0.1
Modify (packages/core/platform/templates/):
namespaces.yaml +1 entry: external-gitea
helmrepositories.yaml +1 entry: gitea → dl.gitea.com/charts (namespace: external-gitea)
helmcharts.yaml +1 entry: external-apps-gitea → ./packages/apps/gitea (namespace: cozy-public)
cozyrds.yaml +1 ApplicationDefinition: gitea (kind Gitea)
Dependencies:
postgres Pattern C Postgres/{{ .Release.Name }}-db
Secret postgres-{{ .Release.Name }}-db-credentials key=gitea → gitea.config.database.PASSWD
Service postgres-{{ .Release.Name }}-db-rw:5432 → gitea.config.database.HOST
redis Pattern C Redis/{{ .Release.Name }}-redis
Secret redis-{{ .Release.Name }}-redis-auth key=password → gitea.config.cache.PASSWORD
Service rfs-redis-{{ .Release.Name }}-redis:26379 → gitea.config.cache.HOST (sentinel URL)
Subcharts disabled in upstream values: postgresql-ha, redis-cluster
Open items:
- Icon not supplied; placeholder will be written. Phase 8 will fail on `base64 < logos/$APP_NAME.svg` until an SVG is placed — Phase 9 validation does not look at the icon itself.
Use AskUserQuestion with three options:
approve — record $PLAN_APPROVED = true, proceed to Phase 6 without per-phase confirmations.
edit — ask which phase to revisit (3 for app metadata, 4 for dependencies, 5 for chart sources), then re-run from there and re-present the plan.
abort — stop. No files are written.
Record $PLAN_APPROVED state; Phases 6, 7, and 8 read it to decide whether to skip their individual write-time confirmations.
Phase 6 — Create app chart skeleton
Create packages/apps/$APP_NAME/ with these files:
Chart.yaml
apiVersion: v2
name: $APP_NAME
description: A Helm chart for $APP_DISPLAY_NAME on Cozystack
type: application
version: 0.0.1
appVersion: "$APP_VERSION"
icon: /logos/$APP_NAME.svg
Makefile
The generated Makefile must export NAME and NAMESPACE — scripts/package.mk has a check: target (a dependency of apply, show, diff, delete, suspend, resume) that exits with env NAME is not set! when either is empty. $NAMESPACE should be the operator namespace when the app depends on one, otherwise use cozy-system.
export NAME=$APP_NAME
export NAMESPACE=<operator-namespace or cozy-system>
include ../../../scripts/package.mk
generate:
cozyvalues-gen --values values.yaml --schema values.schema.json --readme README.md
The reference external-apps-example repo does not ship hack/update-crd.sh — that script lives only in the cozystack monorepo. Do not call it from the generated Makefile. The ApplicationDefinition entry in cozyrds.yaml is composed by hand in Phase 8.
logos/$APP_NAME.svg
If the user provided an icon path, copy it:
mkdir -p $REPO_DIR/packages/apps/$APP_NAME/logos
cp $ICON_PATH $REPO_DIR/packages/apps/$APP_NAME/logos/$APP_NAME.svg
If no icon was provided, create the logos/ directory and print:
Place your app icon at packages/apps/$APP_NAME/logos/$APP_NAME.svg before running make generate.
values.yaml
Use cozyvalues-gen annotation format. Follow the exact style from the cozystack postgres chart:
host: ""
size: 10Gi
storageClass: ""
For Pattern C (sibling cozystack ApplicationDefinition) dependencies — the default for external apps — add one section per dep. The fields must match what Phase 7 templates read. Example for postgres + redis:
database:
size: 10Gi
replicas: 2
user: app
name: app
password: ""
redis:
size: 1Gi
replicas: 2
Add or remove sections to match the actual dependency set resolved in Phase 4. Field names must match .Values.<dep>.* references emitted by Phase 7 templates — re-check after editing.
For Pattern A (in-chart operator CR) dependencies, add:
database:
size: 5Gi
replicas: 2
For Pattern B (external reference) dependencies, add:
postgres:
host: ""
port: 5432
secretName: ""
values.schema.json
Generate via:
cd $REPO_DIR/packages/apps/$APP_NAME && cozyvalues-gen --values values.yaml --schema values.schema.json --readme README.md
If cozyvalues-gen fails, write a minimal valid JSON schema manually based on values.yaml fields. Verify with:
jq . values.schema.json > /dev/null
README.md
Generated by cozyvalues-gen in the same command above. If manual, create a parameters table matching the mongodb example format.
.helmignore
logos/
Phase 7 — Create templates
Create packages/apps/$APP_NAME/templates/ with the following files.
Main workload — $APP_NAME.yaml
Generate the primary workload template. If Phase 3 recorded $CHART_SOURCE = upstream, emit a Flux HelmRelease wrapping the upstream chart — the preferred path. If $CHART_SOURCE = custom, emit a Deployment (or StatefulSet) authored from scratch.
Upstream chart wrapper (preferred)
The HelmRelease registered below references the HelmRepository created in Phase 8 and injects cozystack-wired connection details via values and valuesFrom. valuesFrom is the cleanest way to pipe a password out of a Secret created by a Pattern C sibling CR (see Phase 4 Pattern C, Dependency catalog appendix) directly into the upstream chart's value path — no Deployment env rewriting required.
The example below shows a Gitea-like app with both postgres and redis resolved in Phase 4. Treat it as an illustration, not a template to copy verbatim: emit a dependsOn entry and a values.<subchart>.enabled: false line only for dependencies actually recorded in Phase 4 Step 1/Step 2. An app with postgres only must not carry the redis entries, and vice versa — an unrelated dependsOn target blocks reconciliation until it appears (or forever, if it never will).
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}
spec:
interval: 5m
chart:
spec:
chart: $SOURCE_CHART_NAME
version: $SOURCE_CHART_VERSION
sourceRef:
kind: HelmRepository
name: $SOURCE_REPO_NAME
namespace: $SOURCE_NAMESPACE
dependsOn:
- name: postgres-{{ .Release.Name }}-db
namespace: {{ .Release.Namespace }}
- name: redis-{{ .Release.Name }}-redis
namespace: {{ .Release.Namespace }}
values:
postgresql: { enabled: false }
postgresql-ha: { enabled: false }
redis: { enabled: false }
redis-cluster: { enabled: false }
app:
config:
database:
host: postgres-{{ .Release.Name }}-db-rw
port: 5432
name: {{ .Values.database.name }}
user: {{ .Values.database.user }}
redis:
host: rfs-redis-{{ .Release.Name }}-redis
port: 26379
sentinelMaster: mymaster
valuesFrom:
- kind: Secret
name: postgres-{{ .Release.Name }}-db-credentials
valuesKey: {{ .Values.database.user }}
targetPath: app.config.database.password
- kind: Secret
name: redis-{{ .Release.Name }}-redis-auth
valuesKey: password
targetPath: app.config.redis.password
Replace the app.config.* value paths with the actual schema of your upstream chart — this example uses a generic layout. Common real-world paths: Gitea uses gitea.config.database.HOST/PASSWD, Immich uses immich.env.DB_HOSTNAME/DB_PASSWORD, Nextcloud uses internalDatabase.*. Verify against the upstream chart's values.yaml before wiring.
The referenced HelmRepository must exist in the cluster. Phase 8 registers it using the $SOURCE_* variables gathered in Phase 5.
Custom Deployment (fallback)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
app: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: $APP_NAME
image: $CONTAINER_IMAGE
ports:
- name: http
containerPort: $APP_PORT
env:
resources: {}
Add env vars for each dependency based on the pattern chosen in Phase 4.
For Pattern A (managed postgres) env vars:
env:
- name: $DB_HOST_ENV
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-app
key: host
- name: $DB_PORT_ENV
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-app
key: port
- name: $DB_USERNAME_ENV
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-app
key: username
- name: $DB_PASSWORD_ENV
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-app
key: password
- name: $DB_NAME_ENV
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-app
key: dbname
For Pattern B (external reference) env vars:
env:
- name: $DB_HOST_ENV
value: {{ .Values.postgres.host | quote }}
- name: $DB_PORT_ENV
value: {{ .Values.postgres.port | quote }}
- name: $DB_PASSWORD_ENV
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.secretName }}
key: password
Dependency creation templates (Pattern A and Pattern C)
For each Pattern A or Pattern C dependency recorded in Phase 4, emit one template file under packages/apps/$APP_NAME/templates/, named after the dependency (postgres.yaml, redis.yaml, mariadb.yaml, …). Each template must reflect the CR identity and wiring captured during Phase 4 — do not reuse the postgres pattern for other dependencies.
The Pattern C examples below are the preferred shape for external apps; the Pattern A examples (system-style, in-chart operator CR) follow for the escape-hatch case. For Pattern B there is no per-dep template to emit — the app chart merely reads connection values the user supplies.
postgres.yaml — Pattern C (cozystack Postgres sibling CR)
Reference: cozystack/packages/system/postgres-rd/cozyrds/postgres.yaml (authoritative contract), cozystack/packages/apps/postgres/templates/ (what the downstream chart renders). Pattern C is the cleanest path: the app chart creates one cozystack CR, the controller does the rest.
---
apiVersion: apps.cozystack.io/v1alpha1
kind: Postgres
metadata:
name: {{ .Release.Name }}-db
namespace: {{ .Release.Namespace }}
spec:
size: {{ .Values.database.size }}
replicas: {{ .Values.database.replicas }}
external: false
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
users:
{{ .Values.database.user }}:
{{- with .Values.database.password }}
password: {{ . | quote }}
{{- end }}
databases:
{{ .Values.database.name }}:
roles:
admin:
- {{ .Values.database.user }}
Omit password when the user did not supply one. The downstream packages/apps/postgres/ chart (templates/init-script.yaml) already uses Sprig's lookup to read the existing postgres-{{ .Release.Name }}-db-credentials Secret and reuses the password on subsequent renders. Passing randAlphaNum from this chart would generate a fresh value on every reconcile and silently rotate the Secret mid-run.
Outputs (rendered by the downstream packages/apps/postgres/ chart):
- Secret
postgres-{{ .Release.Name }}-db-credentials — one key per user, value is the password.
- Services
postgres-{{ .Release.Name }}-db-rw, -r, -ro; port 5432.
Consume these from the main workload HelmRelease via values + valuesFrom (see Phase 7 Main workload and Dependency catalog Pattern C appendix).
redis.yaml — Pattern C (cozystack Redis sibling CR)
---
apiVersion: apps.cozystack.io/v1alpha1
kind: Redis
metadata:
name: {{ .Release.Name }}-redis
namespace: {{ .Release.Namespace }}
spec:
size: {{ .Values.redis.size }}
replicas: {{ .Values.redis.replicas }}
external: false
authEnabled: true
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
Outputs (rendered by the downstream packages/apps/redis/ chart):
- Secret
redis-{{ .Release.Name }}-redis-auth — key password.
- Sentinel service
rfs-redis-{{ .Release.Name }}-redis on port 26379.
database.yaml — Pattern A (in-chart CloudNativePG Cluster)
Reference: cozystack/packages/system/harbor/templates/database.yaml.
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: {{ .Release.Name }}-db
spec:
instances: {{ .Values.database.replicas }}
imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie
storage:
size: {{ .Values.database.size }}
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
bootstrap:
initdb:
database: app
owner: app
encoding: UTF8
localeCollate: en_US.UTF-8
localeCType: en_US.UTF-8
monitoring:
enablePodMonitor: true
inheritedMetadata:
labels:
policy.cozystack.io/allow-to-apiserver: "true"
Outputs (auto-created by the CNPG operator; no chart-side Secret):
- Secret
{{ .Release.Name }}-db-app — keys host, port, username, password, dbname, uri, jdbc-uri.
- Services
{{ .Release.Name }}-db-rw (primary), -db-r (read replicas), -db-ro (read-only).
redis.yaml — Pattern A (in-chart Spotahome RedisFailover)
Reference: cozystack/packages/system/harbor/templates/redis.yaml.
The chart creates the Secret before the CR — the operator reads it via spec.auth.secretPath. The password is computed with lookup so an existing Secret is reused across re-renders; without this, randAlphaNum would emit a fresh value on every reconcile and silently rotate the RedisFailover credential. This matches the upstream idiom in cozystack/packages/apps/redis/templates/redisfailover.yaml:
{{- $existing := lookup "v1" "Secret" .Release.Namespace (printf "%s-redis-auth" .Release.Name) }}
{{- $password := randAlphaNum 32 | b64enc }}
{{- if $existing }}
{{- $password = index $existing.data "password" }}
{{- end }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-redis-auth
data:
password: {{ $password }}
---
apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
name: {{ .Release.Name }}-redis
spec:
sentinel:
replicas: 3
redis:
replicas: {{ .Values.redis.replicas }}
storage:
persistentVolumeClaim:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.redis.size }}
{{- with .Values.storageClass }}
storageClassName: {{ . }}
{{- end }}
auth:
secretPath: {{ .Release.Name }}-redis-auth
If the user supplied .Values.redis.password, prefer it over the random value — replace $password := randAlphaNum 32 | b64enc with $password := .Values.redis.password | b64enc under a {{- with .Values.redis.password }} guard, preserving the lookup branch as the upgrade path.
Outputs:
- Secret
{{ .Release.Name }}-redis-auth (chart-created) — key password.
- Sentinel service
rfs-{{ .Release.Name }}-redis on port 26379.
service.yaml (if app exposes a port)
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
type: ClusterIP
ports:
- name: http
port: $APP_PORT
targetPort: http
selector:
app: {{ .Release.Name }}
ingress.yaml (if user requested it)
{{- if .Values.host }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
spec:
rules:
- host: {{ .Values.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}
port:
name: http
{{- end }}
If $PLAN_APPROVED was set in Phase 5.5, write the templates directly. Otherwise use AskUserQuestion to confirm the generated templates before writing them and show a summary of what will be created.
Phase 8 — Register in core/platform
Update five files under packages/core/platform/templates/. Read each file first, then append.
Before generating any YAML in this phase, extract the GitRepository name from init.yaml — it is referenced as sourceRef.name in both the operator HelmRelease and the ApplicationDefinition below. Always select by kind so multi-document init.yaml files (the reference layout has both a GitRepository and a HelmRelease, often sharing a name by coincidence) resolve deterministically:
GIT_REPO_NAME=$(yq -r 'select(.kind == "GitRepository") | .metadata.name' $REPO_DIR/init.yaml)
Stop and ask the user if the extracted value is empty or if the selector returns more than one GitRepository.
namespaces.yaml
Append one namespace per source role recorded in Phase 5:
- Operator source →
external-$APP_NAME-operator (hosts both the operator HelmRelease and its HelmRepository).
- Main source →
external-$APP_NAME (hosts the HelmRepository for the upstream app chart; user instances deployed via the dashboard live in tenant namespaces and reference this HelmRepository cross-namespace).
---
apiVersion: v1
kind: Namespace
metadata:
labels:
cozystack.io/system: "true"
name: $SOURCE_NAMESPACE
Emit one entry for each $SOURCE_NAMESPACE gathered (deduplicate if operator and main share a namespace by mistake).
helmrepositories.yaml
For every source gathered in Phase 5 (operator and/or main), append a HelmRepository. Emit exactly one of the two variants below based on $SOURCE_REPO_TYPE — do not leave commented lines in the output.
HTTPS source ($SOURCE_REPO_TYPE = https):
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: $SOURCE_REPO_NAME
namespace: $SOURCE_NAMESPACE
spec:
interval: 5m
url: $SOURCE_REPO_URL
OCI source ($SOURCE_REPO_TYPE = oci):
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: $SOURCE_REPO_NAME
namespace: $SOURCE_NAMESPACE
spec:
interval: 5m
type: oci
url: $SOURCE_REPO_URL
helmreleases.yaml
Only operator sources get a platform-level HelmRelease here. The main upstream chart (when $CHART_SOURCE = upstream) is wrapped by a HelmRelease rendered inside the app chart itself (Phase 7 Main workload) — one per user-deployed instance — and never appears in packages/core/platform/templates/helmreleases.yaml.
For each operator source:
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: $SOURCE_REPO_NAME
namespace: $SOURCE_NAMESPACE
spec:
interval: 5m
releaseName: $SOURCE_REPO_NAME
targetNamespace: $SOURCE_NAMESPACE
chart:
spec:
chart: $SOURCE_CHART_NAME
sourceRef:
kind: HelmRepository
name: $SOURCE_REPO_NAME
version: '$SOURCE_CHART_VERSION'
If the operator must wait on another operator (e.g., CNPG) before reconciling, add spec.dependsOn here — dependency ordering belongs on user-authored HelmReleases. The application's own HelmRelease is rendered per user-instance from the app chart; it can declare its own dependsOn against sibling cozystack-rendered HelmReleases (see Phase 7 Main workload).
helmcharts.yaml
Always append a flux HelmChart that backs the ApplicationDefinition.spec.release.chartRef below. Without this entry the chartRef has no source and flux cannot produce the chart artifact:
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: $GIT_REPO_NAME-$APP_NAME
namespace: cozy-public
spec:
interval: 5m
chart: ./packages/apps/$APP_NAME
sourceRef:
kind: GitRepository
name: $GIT_REPO_NAME
reconcileStrategy: Revision
The metadata.name here must match the release.chartRef.name in the ApplicationDefinition below, and the metadata.namespace must match release.chartRef.namespace (cozy-public, same as the GitRepository in init.yaml).
cozyrds.yaml
Append an ApplicationDefinition for the app. The openAPISchema must match values.schema.json content.
ApplicationDefinition is cluster-scoped (scope: Cluster in the CRD), so metadata.namespace must be omitted. The release.chartRef field references the flux HelmChart defined in helmcharts.yaml above — both must use the same name and namespace (cozy-public).
---
apiVersion: cozystack.io/v1alpha1
kind: ApplicationDefinition
metadata:
name: $APP_NAME
spec:
application:
kind: $APP_KIND
openAPISchema: |
<contents of values.schema.json, indented by 6 spaces so every line sits under the `|` block scalar>
plural: $APP_PLURAL
singular: $APP_NAME
release:
chartRef:
kind: HelmChart
name: $GIT_REPO_NAME-$APP_NAME
namespace: cozy-public
labels:
cozystack.io/ui: "true"
prefix: $APP_NAME-
dashboard:
category: $CATEGORY
singular: $APP_DISPLAY_NAME
plural: $APP_DISPLAY_PLURAL
description: $APP_DESCRIPTION
tags:
- $TAG1
- $TAG2
icon: $ICON_B64
keysOrder:
- - apiVersion
- - kind
- - metadata
- - metadata
- name
To compute $ICON_B64:
base64 < $REPO_DIR/packages/apps/$APP_NAME/logos/$APP_NAME.svg | tr -d '\n'
To produce the correctly indented openAPISchema block when composing the CRD inline, prefix every line of values.schema.json with six spaces so the JSON becomes a valid child of the | literal scalar:
sed 's/^/ /' $REPO_DIR/packages/apps/$APP_NAME/values.schema.json
Verify the final YAML with yq e '.' cozyrds.yaml > /dev/null before moving on — an off-by-one indentation silently breaks the schema.
If $PLAN_APPROVED was set in Phase 5.5, write the platform-level additions directly. Otherwise use AskUserQuestion to confirm all core/platform changes before writing and show the diff of what will be appended to each file.
Phase 9 — Validation
Run the following checks:
-
Generate schema and README:
cd $REPO_DIR/packages/apps/$APP_NAME && make generate
-
Helm template render:
cd $REPO_DIR/packages/apps/$APP_NAME && helm template test .
Fix any template errors before proceeding.
-
JSON schema validity:
jq . $REPO_DIR/packages/apps/$APP_NAME/values.schema.json > /dev/null
-
YAML validity of cozyrds:
yq e '.' $REPO_DIR/packages/core/platform/templates/cozyrds.yaml > /dev/null
-
Platform chart render:
cd $REPO_DIR/packages/core/platform && helm template test .
If any check fails, fix the issue and re-run. If the fix is not obvious, stop and report the error to the user.
Phase 10 — Summary
Print a report:
- App name:
$APP_NAME
- Files created (list all new files with paths relative to repo root)
- Files modified (list all modified files under
packages/core/platform/templates/: namespaces.yaml, helmrepositories.yaml, helmreleases.yaml, helmcharts.yaml, cozyrds.yaml)
- Dependencies: for each dependency, state the chosen pattern (C: sibling cozystack CR / A: in-chart operator CR / B: external reference) and which Secret/Service the app consumes
- Chart source:
upstream (HelmRelease wrapper; note upstream repo + chart name + version) or custom (hand-written Deployment/StatefulSet)
- Operator: created or not, chart source
- Dashboard: category, tags, icon status (present / missing)
- Next steps for the user:
- Review all generated files
- Place icon SVG if not yet provided:
packages/apps/$APP_NAME/logos/$APP_NAME.svg
- Run
cd packages/apps/$APP_NAME && make generate to regenerate CRD after any values.yaml changes
- Commit and push — Flux picks up changes via the GitRepository defined in
init.yaml (default interval: 1m)
- Verify in cluster:
kubectl get $APP_PLURAL.$APP_NAME.apps.cozystack.io --all-namespaces
Dependency catalog
Contracts are resolved at runtime in Phase 4 Step 2. This appendix is a reference, not a pre-baked list of supported deps — two worked examples below (postgres, redis) show what a correctly resolved $DEP_CONTRACT looks like so the assistant can verify its own parsing and the user can eyeball a familiar case.
For any dependency the skill has not seen before, run Phase 4 Step 2 against cozystack itself. There is no need — and no place — to keep a hand-maintained enumeration of every supported dep; cozystack's packages/system/<dep>-rd/cozyrds/<dep>.yaml is the single source of truth.
Interpreting a resolved contract
An ApplicationDefinition document has this structure (minimal view — see cozystack.io_applicationdefinitions.yaml CRD for the full schema):
apiVersion: cozystack.io/v1alpha1
kind: ApplicationDefinition
metadata:
name: <dep>
spec:
application:
kind: <KindName>
plural: <plural>
openAPISchema: |
{ ... JSON ... }
release:
prefix: <prefix>-
chartRef: ...
secrets:
include:
- resourceNames:
- <prefix>{{ .name }}-<suffix>
services:
include:
- resourceNames:
- <prefix>{{ .name }}-<suffix>
{{ .name }} in the resourceNames is the name of the cozystack-level CR the app chart emits, not the app's own .Release.Name. Concretely: when the app chart emits ${DEP_CONTRACT.kind}/{{ .Release.Name }}-db, substitute {{ .name }} = <app-release-name>-db in every template.
The next two sections show the result of the resolution procedure for postgres and redis — verified against upstream cozystack main at the time this skill was written. When a cozystack release changes these contracts, the sections below go stale; trust runtime resolution, not this appendix.
Pattern C — postgres (cozystack Postgres)
| Field | Value |
|---|
| Sibling CR | apps.cozystack.io/v1alpha1 → Postgres |
| Source of truth | cozystack/packages/system/postgres-rd/cozyrds/postgres.yaml |
| Downstream HelmRelease | postgres-{{ .name }} (prefix postgres- from the ApplicationDefinition) |
| Credentials Secret | postgres-{{ .name }}-credentials — keys are the usernames configured in spec.users.<u>.password. The key's value is the plaintext password. |
| Services | postgres-{{ .name }}-rw (primary), -r (read replicas), -ro (read-only), -external-write (LoadBalancer when spec.external: true) |
| Port | 5432 |
| Sibling CR spec essentials | size, replicas, users (map <username>: {password: ...}), databases (map <dbname>: {roles: {admin: [...usernames]}}), external, storageClass |
Example Pattern C CR (rendered from the app chart's templates/postgres.yaml):
apiVersion: apps.cozystack.io/v1alpha1
kind: Postgres
metadata:
name: {{ .Release.Name }}-db
namespace: {{ .Release.Namespace }}
spec:
size: {{ .Values.database.size }}
replicas: {{ .Values.database.replicas }}
external: false
users:
{{ .Values.database.user }}:
{{- with .Values.database.password }}
password: {{ . | quote }}
{{- end }}
databases:
{{ .Values.database.name }}:
roles:
admin:
- {{ .Values.database.user }}
password is omitted unless the user explicitly supplied one — see the rationale in Phase 7's postgres.yaml section. The downstream packages/apps/postgres/ chart generates and preserves the password itself via lookup.
Wiring in the main workload HelmRelease:
values:
app:
config:
database:
host: postgres-{{ .Release.Name }}-db-rw
port: 5432
name: {{ .Values.database.name }}
user: {{ .Values.database.user }}
valuesFrom:
- kind: Secret
name: postgres-{{ .Release.Name }}-db-credentials
valuesKey: {{ .Values.database.user }}
targetPath: app.config.database.password
Pattern C — redis (cozystack Redis)
| Field | Value |
|---|
| Sibling CR | apps.cozystack.io/v1alpha1 → Redis |
| Source of truth | cozystack/packages/system/redis-rd/cozyrds/redis.yaml |
| Downstream HelmRelease | redis-{{ .name }} (prefix redis-) |
| Credentials Secret | redis-{{ .name }}-auth — key password. Present only when spec.authEnabled: true (default). |
| Services | rfs-redis-{{ .name }} (sentinel :26379), rfrm-redis-{{ .name }} (master), rfrs-redis-{{ .name }} (slaves), redis-{{ .name }}-external-lb (LoadBalancer when spec.external: true) |
| Sibling CR spec essentials | size, replicas, authEnabled, external, version (v8/v7), storageClass |
Example Pattern C CR (rendered from the app chart's templates/redis.yaml):
apiVersion: apps.cozystack.io/v1alpha1
kind: Redis
metadata:
name: {{ .Release.Name }}-redis
namespace: {{ .Release.Namespace }}
spec:
size: {{ .Values.redis.size }}
replicas: {{ .Values.redis.replicas }}
external: false
authEnabled: true
Wiring in the main workload HelmRelease:
values:
app:
config:
redis:
host: rfs-redis-{{ .Release.Name }}-redis
port: 26379
sentinelMaster: mymaster
valuesFrom:
- kind: Secret
name: redis-{{ .Release.Name }}-redis-auth
valuesKey: password
targetPath: app.config.redis.password
Pattern A — research procedure (for dependencies not in the Pattern A catalog below)
-
Locate a reference in the cozystack monorepo:
grep -rlE "kind: (Cluster|RedisFailover|PerconaServerMongoDB|Kafka|ClickHouseInstallation|NATS)" \
$COZYSTACK_REPO/packages/{apps,system}/*/templates/
-
Read the CR template and the consumer template (typically the app's main workload). Extract:
- CR
apiVersion and kind.
- Whether the operator auto-creates a credentials Secret, or the chart must create one itself.
- Exact Secret name template and key names.
- Whether the app wires credentials via
env + secretKeyRef, a mounted volume, or a config file.
-
Record the findings before proceeding to Phase 7. If research does not yield a verified answer, stop and ask the user — do not invent CR shapes or secret key names.
postgres — CloudNativePG Cluster
| Field | Value |
|---|
| Operator | CloudNativePG (cnpg.io) — provided by packages/system/cnpg-operator |
| CR | postgresql.cnpg.io/v1 → Cluster |
| Reference template | cozystack/packages/system/harbor/templates/database.yaml |
| Reference consumer | cozystack/packages/system/keycloak/templates/sts.yaml — the KC_DB_* env block |
| Output Secret | Auto-created by the operator. If cluster is named <release>-db, the Secret is <release>-db-app. |
| Output Secret keys | host, port, username, password, dbname, uri, jdbc-uri |
| Superuser Secret | <release>-db-superuser (same keys + superuser) |
| Services | <release>-db-rw (primary), <release>-db-r (read replicas), <release>-db-ro (read-only) |
| App wiring | env via secretKeyRef to the auto-created Secret |
redis — Spotahome RedisFailover
| Field | Value |
|---|
| Operator | Spotahome Redis Operator (databases.spotahome.com) — provided by packages/system/redis-operator |
| CR | databases.spotahome.com/v1 → RedisFailover |
| Reference template | cozystack/packages/system/harbor/templates/redis.yaml |
| Output Secret | Not auto-created — the chart itself creates a Secret alongside the CR (naming is chart-choice, commonly <release>-redis-auth with key password). |
| CR ↔ Secret wiring | spec.auth.secretPath: <secret-name> — the operator reads the Secret by that name. |
| App wiring | The same Secret is mounted or read via env by the app. The chart generates the password (e.g., from .Values.redis.password or a randomly generated one) and stores it in the Secret. |
Unlike CNPG, the Spotahome operator does NOT emit connection details. The chart is responsible for password generation and for wiring the same Secret into both the operator (auth.secretPath) and the consuming app.
mongodb — Percona PerconaServerMongoDB
| Field | Value |
|---|
| Operator | Percona Server for MongoDB (psmdb.percona.com) — provided by packages/system/psmdb-operator |
| CR | psmdb.percona.com/v1 → PerconaServerMongoDB |
| Reference template | cozystack/packages/apps/mongodb/templates/mongodb.yaml |
| Seed Secret | Chart-created, referenced via spec.secrets.users — the operator reads this for initial user/password seeding. |
| App wiring | Depends on the app — typically a DATABASE_URL assembled from the Secret. Verify against the specific app's expected env before wiring. |
kafka — Strimzi Kafka
| Field | Value |
|---|
| Operator | Strimzi (kafka.strimzi.io) |
| CR | kafka.strimzi.io/v1beta2 → Kafka (plus KafkaUser for SCRAM/TLS) |
| Reference template | cozystack/packages/apps/kafka/templates/kafka.yaml |
| Output Secrets | Brokers expose services. Client credentials are issued per KafkaUser CR — Strimzi creates a Secret named <KafkaUser-name> with password (SCRAM) and/or user.crt/user.key (TLS). |
| App wiring | SCRAM-SHA-512 via env, or TLS via mounted volume. Consult the KafkaUser status to discover the actual Secret layout. |
Guardrails
- Never commit or push on behalf of the user. This is a generate-only skill.
- Never apply anything to a cluster — no
kubectl apply, no helm install, no make apply. This skill only creates files.
- Never overwrite existing
packages/apps/$APP_NAME/ without explicit user confirmation.
- Never skip Phase 4 Step 1 (chart requirement analysis) or Step 2 (contract resolution). Every Pattern C dependency must have a
$DEP_CONTRACT record with kind, prefix, secretTemplates, serviceTemplates, and specSchema resolved from a cozystack source before Phase 7 emits any template. No speculation — if resolution fails, Pattern C is unavailable for that dep.
- Never guess a dependency's CR shape, Secret name, or Secret keys. For Pattern A deps the operator-CR research step is mandatory — use the Pattern A catalog first; for anything not in the catalog, open a reference implementation in the cozystack monorepo before writing templates. If research does not yield a verified answer, stop and ask.
- Never copy the postgres/CNPG wiring onto a different dependency. CNPG auto-creates the credentials Secret; Spotahome RedisFailover does not (the chart creates it). Other operators differ further — always verify.
- Never edit files in a cozystack checkout used as reference — those are read-only.
- Never modify
init.yaml — the user manages their GitRepository and root HelmRelease manually.
- Always gate file creation behind user confirmation. The consolidated gate is Phase 5.5 (Present plan); once
$PLAN_APPROVED is set, Phases 6-8 proceed without further prompts. If Phase 5.5 was skipped or the plan was not approved, fall back to per-phase AskUserQuestion confirmations in Phases 6, 7, and 8.
- Always read existing files before appending to them (
namespaces.yaml, helmrepositories.yaml, helmreleases.yaml, helmcharts.yaml, cozyrds.yaml).
- If
cozyvalues-gen is not installed, do not attempt to generate schema/README manually beyond a minimal placeholder. Tell the user to install it and re-run make generate.
References
Read these files on demand when reasoning about structure and conventions:
packages/core/platform/templates/cozyrds.yaml — existing ApplicationDefinition entries, structure reference
packages/core/platform/templates/helmreleases.yaml — existing HelmRelease entries for operators
packages/core/platform/templates/helmrepositories.yaml — existing HelmRepository entries for operator chart sources
packages/core/platform/templates/helmcharts.yaml — existing HelmChart entries that back each app's release.chartRef
packages/core/platform/templates/namespaces.yaml — existing namespace entries
scripts/package.mk — make targets: show, apply, diff, suspend, resume, delete. Requires NAME and NAMESPACE exports.
init.yaml — GitRepository name and root HelmRelease (needed for sourceRef in CRD and HelmRelease)
packages/system/<dep>-rd/cozyrds/<dep>.yaml — authoritative contract per dep. Resolved at runtime by Phase 4 Step 2 from $COZYSTACK_REPO, gh api repos/cozystack/cozystack, or kubectl get applicationdefinition <dep> in that order.
- Cozystack external apps docs: https://cozystack.io/docs/applications/external/
- Flux HelmRelease spec (dependsOn): https://fluxcd.io/flux/components/helm/helmreleases/
- CloudNativePG Cluster CRD: https://cloudnative-pg.io/documentation/current/cloudnative-pg.v1/
- CNPG bootstrap initdb: https://cloudnative-pg.io/documentation/current/bootstrap/#initdb
cozystack/packages/apps/postgres/values.yaml — reference for cozyvalues-gen annotation style (@param, @typedef, @field, @enum, @section)
cozystack/packages/system/harbor/templates/database.yaml — reference Pattern A: managed CNPG Cluster in chart templates
cozystack/packages/system/keycloak/templates/sts.yaml — reference Pattern A: the KC_DB_* env block consumes a CNPG Secret via secretKeyRef with keys host, port, username, password, dbname
cozystack/packages/apps/harbor/templates/harbor.yaml — reference Pattern A: the database.external values block wires existingSecret and points host at the CNPG -rw service