{"id":50886192,"url":"https://github.com/hops-ops/email-marketing-stack","last_synced_at":"2026-06-15T17:04:43.121Z","repository":{"id":360111015,"uuid":"1248748069","full_name":"hops-ops/email-marketing-stack","owner":"hops-ops","description":"EmailMarketingStack — Crossplane composition that installs Listmonk + embedded CNPG + optional OIDC/SMTP/exposure + chart-side post-install api-user-bootstrap (v3+). Pairs with terraform-provider-listmonk.","archived":false,"fork":false,"pushed_at":"2026-05-25T02:41:45.000Z","size":38,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-25T04:26:29.397Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go Template","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hops-ops.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-25T02:36:34.000Z","updated_at":"2026-05-25T02:40:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hops-ops/email-marketing-stack","commit_stats":null,"previous_names":["hops-ops/email-marketing-stack"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hops-ops/email-marketing-stack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Femail-marketing-stack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Femail-marketing-stack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Femail-marketing-stack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Femail-marketing-stack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hops-ops","download_url":"https://codeload.github.com/hops-ops/email-marketing-stack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Femail-marketing-stack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34372174,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-15T17:04:42.464Z","updated_at":"2026-06-15T17:04:43.114Z","avatar_url":"https://github.com/hops-ops.png","language":"Go Template","funding_links":[],"categories":[],"sub_categories":[],"readme":"# email-marketing-stack\n\nInstalls [Listmonk](https://listmonk.app) into a Kubernetes cluster as the\nplatform's self-hosted newsletter + marketing-campaign engine. Wraps the\n[`redzumi/listmonk-chart`](https://redzumi.github.io/listmonk-chart) chart\nwith a typed `EmailMarketingStack` XRD that handles:\n\n- **Database**: embedded `PSQLCluster` (CNPG) by default — Listmonk reads\n  its `db__user` / `db__password` directly from the CNPG-generated app\n  Secret via the chart's `existingSecret` field.\n- **SMTP**: an `ExternalSecret` that pulls SES SMTP credentials from\n  AWS Secrets Manager. The remote path is observed from the referenced\n  `SMTPStack.status.smtp.awsSecretsManagerPath` — composition gates on\n  the observed status, so the ES renders only after `SMTPStack` reports\n  the path.\n\nSee the spec at `specs/email-marketing-stack` in the GitKB for the full\ndesign (per-tenant model, OIDC, exposure, observability, analytics bridge,\nprovider workstream).\n\n## Scope: v1\n\n| Composed | What | Notes |\n| --- | --- | --- |\n| `Namespace` | The Listmonk install namespace (default `marketing`) | Owned by the stack. |\n| `PSQLCluster` | CNPG-backed Postgres for Listmonk | `targetNamespace` = install ns so the app Secret is mounted natively. |\n| `Helm Release` | redzumi/listmonk-chart 2.0.1 (Listmonk v6.0.0) | Embedded Postgres subchart **off**; chart wires to CNPG via `database.existingSecret` (`passwordKey: password`). |\n| `ExternalSecret` (Object MR) | `smtp-{host,port,username,password,from}` projected into `listmonk-smtp` K8s Secret | Matches the chart's `smtp.existingSecret` contract. Renders only when `spec.smtp.smtpStackRef.name` is set AND the observed SMTPStack reports an AWS SM path. |\n| `Object` (Observe) | Reads upstream `SMTPStack` | `managementPolicies: [Observe]` — composition reads `status.smtp.awsSecretsManagerPath`. |\n| `Usage` × 2 | Delete-order safeguards | Helm release drains before namespace or PSQLCluster teardown. |\n\n### Deferred (v2+)\n\n- OIDC + Gateway API exposure (`/admin`, `/api` gated; `/subscription/*`,\n  `/campaign/*`, `/link/*`, `/uploads/*` anonymous).\n- `meysam81/listmonk-exporter` sidecar + the two Grafana dashboards in\n  `xrs/stacks/aws/observe/dashboards/email-marketing/`.\n- S3 media backend, SES bounce ingestion.\n- `tenancy.mode=tenant` integration with TenantStack.\n- `provider-listmonk` declarative content management\n  (`Template` / `List` / `Campaign` / `Sequence` / `Settings` / `User`).\n\n## Known chart gap (OQ1 follow-up)\n\nThe pinned upstream chart (`redzumi/listmonk-chart` 2.0.1) accepts\n`smtp.existingSecret` and synthesizes a placeholder Secret when none is\nsupplied, but the Deployment template does **not** project the SMTP\nsecret values into env vars. The `listmonk-smtp` K8s Secret we render\nmaterializes correctly, but the running Pod won't consume it\nautomatically.\n\n**v1 operator path**: configure SMTP via the Listmonk admin UI\n(Settings → SMTP) — paste the values from the K8s Secret:\n\n```bash\nkubectl -n marketing get secret listmonk-smtp -o json \\\n  | jq '.data | map_values(@base64d)'\n```\n\n**v2 plan**: fork the chart into `hops-ops/helm-charts/listmonk` and add\n`envFrom: secretRef` on the SMTP Secret so the existing ExternalSecret\nwires the Pod declaratively. The Secret key layout (`smtp-host`,\n`smtp-port`, `smtp-username`, `smtp-password`, `smtp-from`) already\nmatches the chart's `smtp.existingSecret` contract, so the fork only\nneeds to add the envFrom block — no ExternalSecret changes required.\nTracked in the spec under `OQ1` + `OQ10`.\n\n### Chart selection note\n\nWe evaluated `th0th/helm-charts/listmonk` (5.0.3-3, Listmonk v5.0.3) but\nits `values.schema.json` carries an external `$ref` to\n`raw.githubusercontent.com` that the provider-helm-bundled `helm` CLI\nrejects with \"invalid file url\", failing every install. The redzumi\nchart ships no `values.schema.json` and uses Listmonk v6.0.0 — newer\napp + works through provider-helm without modification.\n\n## First-run admin\n\nThe chart's `initContainer` runs `listmonk --install --idempotent --yes`\nwhich creates the database schema; Listmonk's web UI prompts for the\nadmin username + password on first HTTP visit. There is no env-var\nadmin bootstrap — operator sets credentials interactively. v2 can switch\nto OIDC (Zitadel) once the AuthStack `listmonk` Application is provisioned.\n\n## Local-dev: pat-local (colima)\n\nPat-local's `helm`/`kubernetes` ProviderConfigs target the AWS EKS data\nplane (see `reference_pat_local_providerconfig_target`). With Crossplane\nrunning on colima:\n\n```bash\n# Install the configuration package\nhops config install --path xrs/stacks/k8s/email-marketing\n\n# Apply the local example\nkubectl --context pat-local apply -f xrs/stacks/k8s/email-marketing/examples/emailmarketingstacks/local-colima.yaml\n\n# Watch the XR\nkubectl --context pat-local get emailmarketingstack -A -w\n\n# Port-forward the admin UI once the Helm release is Ready\nkubectl --context pat-local -n marketing port-forward svc/marketing-listmonk 9000:9000\n# → open http://localhost:9000/admin (default credentials: admin / set at first run)\n```\n\nThe `local-colima.yaml` example references an `SMTPStack` named `smtp` in\nnamespace `default`. Until that XR lands on the cluster, the\n`marketing-observed-smtp-stack` Object MR will be in a \"not found\" state\nand the SMTP ExternalSecret will not render. Install + admin UI still\nwork; SMTP wiring catches up automatically once `SMTPStack` reports\n`status.smtp.awsSecretsManagerPath`.\n\n## Workflows\n\n| Command | What it does |\n| --- | --- |\n| `make render:all` | Render every example, parallelized |\n| `make validate:all` | Same as render + `crossplane beta validate` |\n| `make test` | Run the KCL `CompositionTest` cases under `tests/` |\n| `make build` | `up project build` — emits the configuration package |\n| `make clean` | Drop `_output/` + `.up/` + `apis/.../configuration.yaml` |\n| `make publish tag=vX.Y.Z` | Push the configuration package to the registry |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhops-ops%2Femail-marketing-stack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhops-ops%2Femail-marketing-stack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhops-ops%2Femail-marketing-stack/lists"}