<memvid-mind-context>
# 🧠 Claude Mind Ready

📁 Project: **churn-renewal-forecast**
💾 Memory will be created at: `.claude/mind.mv2`

_Your observations will be automatically captured._
</memvid-mind-context>


<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.

This is not negotiable. This is not optional. You cannot rationalize your way out of this.
</EXTREMELY-IMPORTANT>

## Instruction Priority

Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:

1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority

If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.

## How to Access Skills

**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.

**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.

**In other environments:** Check your platform's documentation for how skills are loaded.

## Platform Adaptation

Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.

# Using Skills

## The Rule

**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.

```dot
digraph skill_flow {
    "User message received" [shape=doublecircle];
    "About to EnterPlanMode?" [shape=doublecircle];
    "Already brainstormed?" [shape=diamond];
    "Invoke brainstorming skill" [shape=box];
    "Might any skill apply?" [shape=diamond];
    "Invoke Skill tool" [shape=box];
    "Announce: 'Using [skill] to [purpose]'" [shape=box];
    "Has checklist?" [shape=diamond];
    "Create TodoWrite todo per item" [shape=box];
    "Follow skill exactly" [shape=box];
    "Respond (including clarifications)" [shape=doublecircle];

    "About to EnterPlanMode?" -> "Already brainstormed?";
    "Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
    "Already brainstormed?" -> "Might any skill apply?" [label="yes"];
    "Invoke brainstorming skill" -> "Might any skill apply?";

    "User message received" -> "Might any skill apply?";
    "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
    "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
    "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
    "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
    "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
    "Has checklist?" -> "Follow skill exactly" [label="no"];
    "Create TodoWrite todo per item" -> "Follow skill exactly";
}
```

## Red Flags

These thoughts mean STOP—you're rationalizing:

| Thought | Reality |
|---------|---------|
| "This is just a simple question" | Questions are tasks. Check for skills. |
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
| "Let me gather information first" | Skills tell you HOW to gather information. |
| "This doesn't need a formal skill" | If a skill exists, use it. |
| "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. |
| "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |

## Skill Priority

When multiple skills could apply, use this order:

1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution

"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → debugging first, then domain-specific skills.

## Skill Types

**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.

**Flexible** (patterns): Adapt principles to context.

The skill itself tells you which.

## User Instructions

Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.

</EXTREMELY_IMPORTANT>


<memvid-mind-context>
# 🧠 Claude Mind Ready

📁 Project: **churn-renewal-forecast**
💾 Memory will be created at: `.claude/mind.mv2`

_Your observations will be automatically captured._
</memvid-mind-context>

<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.

This is not negotiable. This is not optional. You cannot rationalize your way out of this.
</EXTREMELY-IMPORTANT>

## Instruction Priority

Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:

1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority

If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.

## How to Access Skills

**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.

**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.

**In other environments:** Check your platform's documentation for how skills are loaded.

## Platform Adaptation

Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.

# Using Skills

## The Rule

**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.

```dot
digraph skill_flow {
    "User message received" [shape=doublecircle];
    "About to EnterPlanMode?" [shape=doublecircle];
    "Already brainstormed?" [shape=diamond];
    "Invoke brainstorming skill" [shape=box];
    "Might any skill apply?" [shape=diamond];
    "Invoke Skill tool" [shape=box];
    "Announce: 'Using [skill] to [purpose]'" [shape=box];
    "Has checklist?" [shape=diamond];
    "Create TodoWrite todo per item" [shape=box];
    "Follow skill exactly" [shape=box];
    "Respond (including clarifications)" [shape=doublecircle];

    "About to EnterPlanMode?" -> "Already brainstormed?";
    "Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
    "Already brainstormed?" -> "Might any skill apply?" [label="yes"];
    "Invoke brainstorming skill" -> "Might any skill apply?";

    "User message received" -> "Might any skill apply?";
    "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
    "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
    "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
    "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
    "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
    "Has checklist?" -> "Follow skill exactly" [label="no"];
    "Create TodoWrite todo per item" -> "Follow skill exactly";
}
```

## Red Flags

These thoughts mean STOP—you're rationalizing:

| Thought | Reality |
|---------|---------|
| "This is just a simple question" | Questions are tasks. Check for skills. |
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
| "Let me gather information first" | Skills tell you HOW to gather information. |
| "This doesn't need a formal skill" | If a skill exists, use it. |
| "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. |
| "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |

## Skill Priority

When multiple skills could apply, use this order:

1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution

"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → debugging first, then domain-specific skills.

## Skill Types

**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.

**Flexible** (patterns): Adapt principles to context.

The skill itself tells you which.

## User Instructions

Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.

</EXTREMELY_IMPORTANT>


1 I need to create a Kustomize deployment for the `churn-renewal-forecast` project, modeled after the `saas-tenant-manager` structure.
    2
    3 Please generate the directory structure and file contents for `deploy/kubernetes/` based on the following specifications.
    4
    5 ### **1. Project Specifications**
    6 - **App Name:** `churn-renewal-forecast`
    7 - **Framework:** Next.js (TypeScript) + Prisma (PostgreSQL)
    8 - **Background Jobs:** `pg-boss` (PostgreSQL-backed worker)
    9 - **Namespace:** `churn-renewal-forecast`
   10 - **Vault Path:** `/internal/churn-renewal-forecast`
   11 - **Ingress Host:** `churn-forecast.infrastructure.immuta.io`
   12
   13 ### **2. Components to Create**
   14
   15 **A. Base Configuration (`base/`)**
   16 - **`bootstrap/`**:
   17     - `namespace.yaml`, `serviceaccount.yaml`.
   18     - `secret.yaml`: `ExternalSecret` (vault) mapping: `DATABASE_URL`, `CLERK_SECRET_KEY`, `FRONT_API_TOKEN`, `FRONT_API_TOKEN_2`,
      `PRODUCTBOARD_API_KEY`, `SLACK_BOT_TOKEN`.
   19     - `app-config.env`: ConfigMap values for `APP_URL`, `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE`,
      `SLACK_TEST_CHANNEL`, and PG pool settings (`PG_POOL_MAX`, etc.).
   20 - **`init/`**:
   21     - `job.yaml`: A one-shot Job for Prisma migrations (`bunx prisma migrate deploy`).
   22 - **`netpol/`**:
   23     - `CiliumNetworkPolicy` files for: DNS (UDP 53), RDS (TCP 5432), and External Egress (TCP 443) to `clerk.com`, `api.frontapp.com`,
      `api.productboard.com`, and `slack.com`.
   24 - **`services/`**:
   25     - `web-deployment.yaml`: Deployment for the Next.js app (`next start`).
   26     - `worker-deployment.yaml`: Deployment for the background worker (`bunx tsx src/worker/index.ts`).
   27     - `service.yaml` & `ingress.yaml`: ClusterIP service and Nginx Ingress.
   28
   29 **B. Cluster Overlay (`deploy/eks-shared-infra-use1-2/`)**
   30 - Replicate the overlay pattern from `saas-tenant-manager`, including `.app.yaml` metadata, `ingress-patch.yaml` (private visibility),
      and `serviceaccount.yaml` (IAM role annotation).
   31
   32 ### **3. Key Requirements**
   33 - **Labels:** Use `app.kubernetes.io/name: churn-renewal-forecast` consistently.
   34 - **Resources:** Standard requests/limits (e.g., 256Mi/512Mi).
   35 - **Probes:** Add Liveness/Readiness probes to the web deployment on `/api/health/live` and `/api/health/ready`.
   36 - **Environment:** Both `web` and `worker` must use `envFrom` to pull from the same ConfigMap and Secret.
   37
   38 Please provide the output as a set of files organized by their path.


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Fast read-only search agent for locating code. Use it to find files by pattern (eg. "src/components/**/*.tsx"), grep for symbols or keywords (eg. "API endpoints"), or answer "where is X defined / which files reference Y." Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: "quick" for a single targeted lookup, "medium" for moderate exploration, or "very thorough" to search across multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


## claude.ai Context7
Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.


- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:stats: Show memory statistics and storage information
- mind:search: Search memories for specific content or patterns
- mind:ask: Ask questions about memories and get context-aware answers
- mind:recent: Show recent memories and activity timeline
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


1 I need to create a Kustomize deployment for the `churn-renewal-forecast` project, modeled after the `saas-tenant-manager` structure.     2     3 Please generate the directory structure and file cont…


Create Kustomize deployment for churn-renewal-forecast


Launching skill: superpowers:brainstorming


Base directory for this skill: /Users/steve.briggs/.claude/plugins/cache/claude-plugins-official/superpowers/5.1.0/skills/brainstorming

# Brainstorming Ideas Into Designs

Help turn ideas into fully formed designs and specs through natural collaborative dialogue.

Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.

<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>

## Anti-Pattern: "This Is Too Simple To Need A Design"

Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.

## Checklist

You MUST create a task for each of these items and complete them in order:

1. **Explore project context** — check files, docs, recent commits
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan

## Process Flow

```dot
digraph brainstorming {
    "Explore project context" [shape=box];
    "Visual questions ahead?" [shape=diamond];
    "Offer Visual Companion\n(own message, no other content)" [shape=box];
    "Ask clarifying questions" [shape=box];
    "Propose 2-3 approaches" [shape=box];
    "Present design sections" [shape=box];
    "User approves design?" [shape=diamond];
    "Write design doc" [shape=box];
    "Spec self-review\n(fix inline)" [shape=box];
    "User reviews spec?" [shape=diamond];
    "Invoke writing-plans skill" [shape=doublecircle];

    "Explore project context" -> "Visual questions ahead?";
    "Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
    "Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
    "Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
    "Ask clarifying questions" -> "Propose 2-3 approaches";
    "Propose 2-3 approaches" -> "Present design sections";
    "Present design sections" -> "User approves design?";
    "User approves design?" -> "Present design sections" [label="no, revise"];
    "User approves design?" -> "Write design doc" [label="yes"];
    "Write design doc" -> "Spec self-review\n(fix inline)";
    "Spec self-review\n(fix inline)" -> "User reviews spec?";
    "User reviews spec?" -> "Write design doc" [label="changes requested"];
    "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
```

**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.

## The Process

**Understanding the idea:**

- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria

**Exploring approaches:**

- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why

**Presenting the design:**

- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense

**Design for isolation and clarity:**

- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.

**Working in existing codebases:**

- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.

## After the Design

**Documentation:**

- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
  - (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git

**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:

1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.

Fix any issues inline. No need to re-review — just fix and move on.

**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:

> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."

Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.

**Implementation:**

- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.

## Key Principles

- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense

## Visual Companion

A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.

**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"

**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.

**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**

- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions

A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.

If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`


Bet, Sir Charles. Using brainstorming to make sure we nail this before we touch a single file fr fr.

Let me first vibe check the project context.


ToolSearch

Question: select:TaskCreate


Question: select:TaskCreate


TaskCreate

Explore project context

Check existing deploy/ directory, docs, and project structure to understand current patterns


Task #1 created successfully: Explore project context

Explore project context


Ask clarifying questions

One at a time, understand any gaps in the spec before designing


Task #2 created successfully: Ask clarifying questions

Ask clarifying questions


Propose approaches and present design

Present the Kustomize structure design for approval


Task #3 created successfully: Propose approaches and present design

Propose approaches and present design


Write design doc

Save spec to docs/superpowers/specs/YYYY-MM-DD-kustomize-deployment-design.md and commit


Transition to writing-plans

Invoke writing-plans skill to create implementation plan


Task #4 created successfully: Write design doc

Write design doc


Task #5 created successfully: Transition to writing-plans

Transition to writing-plans


ToolSearch

Question: select:TaskUpdate


Question: select:TaskUpdate


Updated task #1 status


Bash

List root project files


ls /Users/steve.briggs/projects/churn-renewal-forecast/deploy 2>/dev/null || echo "No deploy/ directory found"

Check for existing deploy directory


No deploy/ directory found


drwxr-xr-x steve.briggs staff 128 B  Tue Mar 24 14:46:27 2026 .github
.rw-r--r-- steve.briggs staff   3 B  Tue Mar 24 14:46:27 2026 .nvmrc
.rw-r--r-- steve.briggs staff 146 B  Tue Mar 24 14:46:27 2026 postcss.config.mjs
.rw-r--r-- steve.briggs staff 777 B  Tue Mar 24 14:46:27 2026 tsconfig.json
.rw-r--r-- steve.briggs staff  43 B  Tue Mar 24 14:46:27 2026 vitest.setup.ts
.rw-r--r-- steve.briggs staff 309 B  Tue Mar 24 14:46:27 2026 worker.log
drwxr-xr-x steve.briggs staff 1.8 KB Thu May 14 15:05:24 2026 ..
drwxr-xr-x steve.briggs staff 128 B  Mon May 18 16:11:28 2026 .claude
drwxr-xr-x steve.briggs staff  96 B  Mon May 18 16:11:28 2026 .config
.rw-r--r-- steve.briggs staff 1.3 KB Mon May 18 16:11:28 2026 .env.example
.rw-r--r-- steve.briggs staff 639 B  Mon May 18 16:11:28 2026 .gitignore
.rw-r--r-- steve.briggs staff  22 B  Mon May 18 16:11:28 2026 .npmrc
.rw-r--r-- steve.briggs staff 324 B  Mon May 18 16:11:28 2026 .worktreeinclude
.rw-r--r-- steve.briggs staff  10 KB Mon May 18 16:11:28 2026 CLAUDE.md
.rw-r--r-- steve.briggs staff  12 KB Mon May 18 16:11:28 2026 README.md
.rw-r--r-- steve.briggs staff 187 KB Mon May 18 16:11:28 2026 bun.lock
.rw-r--r-- steve.briggs staff 412 B  Mon May 18 16:11:28 2026 docker-compose.yml
drwxr-xr-x steve.briggs staff 320 B  Mon May 18 16:11:28 2026 docs
.rw-r--r-- steve.briggs staff 1.3 KB Mon May 18 16:11:28 2026 eslint.config.mjs
.rw-r--r-- steve.briggs staff  36 B  Mon May 18 16:11:28 2026 mise.toml
.rw-r--r-- steve.briggs staff 1.5 KB Mon May 18 16:11:28 2026 next.config.mjs
.rw-r--r-- steve.briggs staff 2.6 KB Mon May 18 16:11:28 2026 package.json
.rw-r--r-- steve.briggs staff 263 B  Mon May 18 16:11:28 2026 prisma.config.ts
drwxr-xr-x steve.briggs staff 128 B  Mon May 18 16:11:28 2026 prisma
drwxr-xr-x steve.briggs staff 384 B  Mon May 18 16:11:28 2026 scripts
drwxr-xr-x steve.briggs staff 288 B  Mon May 18 16:11:28 2026 src
drwxr-xr-x steve.briggs staff 928 B  Mon May 18 16:11:28 2026 .
.rw-r--r-- steve.briggs staff 1.2 KB Mon May 18 16:11:28 2026 vitest.config.ts
drwxr-xr-x steve.briggs staff 448 B  Mon May 18 16:11:28 2026 .git


ls /Users/steve.briggs/projects/churn-renewal-forecast/docs/ && ls /Users/steve.briggs/projects/churn-renewal-forecast/src/worker/ 2>/dev/null || echo "No worker dir"

Check docs and worker directory structure


1	# Database
2	DATABASE_URL="postgresql://user:password@localhost:5433/churn_forecast?schema=public"
3
4	# Application URL (used for links in Slack notifications)
5	APP_URL=http://localhost:3000
6
7	# Clerk Authentication
8	# Get these from https://dashboard.clerk.com
9	NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
10	CLERK_SECRET_KEY=sk_test_xxx
11	NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
12	NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
13	NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
14	NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
15
16	# Clerk SSO (SAML) — configured in Clerk Dashboard, no additional env vars needed.
17	# See: https://clerk.com/docs/authentication/saml/overview
18
19	# Front App API Token (from Front Settings > API & integrations)
20	FRONT_API_TOKEN=
21	# Optional second Front API token - doubles sync throughput (6 concurrent sub-jobs instead of 3)
22	FRONT_API_TOKEN_2=
23	# Requests per minute cap for Front API calls (default: 40; max: 50)
24	FRONT_RATE_LIMIT_RPM=40
25
26	# Productboard integration
27	PRODUCTBOARD_API_KEY=
28	NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE=
29
30	# Slack notifications
31	SLACK_BOT_TOKEN=
32	# Channel for sending test/dev notifications (optional; leave empty in production)
33	SLACK_TEST_CHANNEL=
34
35	# PostgreSQL connection pool tuning (optional; defaults are usually fine)
36	PG_POOL_MAX=10
37	PG_POOL_IDLE_TIMEOUT_MS=30000
38	PG_POOL_CONNECTION_TIMEOUT_MS=2000
39

# Database
DATABASE_URL="postgresql://user:password@localhost:5433/churn_forecast?schema=public"

# Application URL (used for links in Slack notifications)
APP_URL=http://localhost:3000

# Clerk Authentication
# Get these from https://dashboard.clerk.com
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/

# Clerk SSO (SAML) — configured in Clerk Dashboard, no additional env vars needed.
# See: https://clerk.com/docs/authentication/saml/overview

# Front App API Token (from Front Settings > API & integrations)
FRONT_API_TOKEN=
# Optional second Front API token - doubles sync throughput (6 concurrent sub-jobs instead of 3)
FRONT_API_TOKEN_2=
# Requests per minute cap for Front API calls (default: 40; max: 50)
FRONT_RATE_LIMIT_RPM=40

# Productboard integration
PRODUCTBOARD_API_KEY=
NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE=

# Slack notifications
SLACK_BOT_TOKEN=
# Channel for sending test/dev notifications (optional; leave empty in production)
SLACK_TEST_CHANNEL=

# PostgreSQL connection pool tuning (optional; defaults are usually fine)
PG_POOL_MAX=10
PG_POOL_IDLE_TIMEOUT_MS=30000
PG_POOL_CONNECTION_TIMEOUT_MS=2000


.rw-r--r-- steve.briggs staff 2.1 KB Tue Mar 24 14:46:27 2026 hydration-guidelines.md
.rw-r--r-- steve.briggs staff 3.2 KB Tue Mar 24 14:46:27 2026 ui-conventions.md
.rw-r--r-- steve.briggs staff  13 KB Mon May 18 16:11:28 2026 architecture.md
.rw-r--r-- steve.briggs staff  14 KB Mon May 18 16:11:28 2026 data-model.md
.rw-r--r-- steve.briggs staff 3.4 KB Mon May 18 16:11:28 2026 database-guidelines.md
drwxr-xr-x steve.briggs staff 6.3 KB Mon May 18 16:11:28 2026 design-docs
.rw-r--r-- steve.briggs staff 2.9 KB Mon May 18 16:11:28 2026 git-workflow.md
drwxr-xr-x steve.briggs staff 320 B  Mon May 18 16:11:28 2026 .
.rw-r--r-- steve.briggs staff 5.2 KB Mon May 18 16:11:28 2026 test-optimization-patterns.md
drwxr-xr-x steve.briggs staff 928 B  Mon May 18 16:11:28 2026 ..
drwxr-xr-x steve.briggs staff 288 B  Mon May 18 16:11:28 2026 ..
drwxr-xr-x steve.briggs staff 480 B  Mon May 18 16:11:28 2026 handlers
drwxr-xr-x steve.briggs staff 128 B  Mon May 18 16:11:28 2026 .
.rw-r--r-- steve.briggs staff 3.9 KB Mon May 18 16:11:28 2026 index.ts


Good context. Let me check the package.json and worker entrypoint to nail down container commands, and also look at the Dockerfile if one exists.


1	{
2	  "name": "churn-renewal-forecast",
3	  "version": "1.0.0",
4	  "private": true,
5	  "trustedDependencies": ["@prisma/client", "@prisma/engines", "prisma"],
6	  "scripts": {
7	    "dev": "next dev --turbopack",
8	    "dev:debug-mem": "NODE_OPTIONS='--heapsnapshot-near-heap-limit=2 --max-old-space-size=6144' next dev --turbopack",
9	    "dev:full": "concurrently \"bun run dev\" \"bun run worker:dev\" --names \"dev,worker\" --prefix-colors \"cyan,magenta\"",
10	    "build": "next build",
11	    "start": "next start",
12	    "lint": "eslint . --cache --cache-location .eslintcache --cache-strategy content",
13	    "test": "vitest",
14	    "test:run": "vitest run",
15	    "test:coverage": "vitest run --coverage",
16	    "worker": "bunx tsx src/worker/index.ts",
17	    "worker:dev": "dotenv -e .env.local -- bunx tsx watch src/worker/index.ts",
18	    "postinstall": "prisma generate",
19	    "db:generate": "prisma generate",
20	    "db:push": "prisma db push",
21	    "db:studio": "prisma studio",
22	    "db:start": "docker compose up -d postgres",
23	    "db:stop": "docker compose stop postgres"
24	  },
25	  "dependencies": {
26	    "@clerk/nextjs": "^7.3.3",
27	    "@prisma/adapter-pg": "^7.8.0",
28	    "@prisma/client": "^7.8.0",
29	    "@slack/web-api": "^7.15.2",
30	    "clsx": "^2.1.1",
31	    "next": "^16.2.6",
32	    "pg-boss": "^12.18.2",
33	    "react": "^19.2.6",
34	    "react-dom": "^19.2.6",
35	    "recharts": "^3.8.1",
36	    "string-strip-html": "^13.5.3",
37	    "swr": "^2.4.1",
38	    "tailwind-merge": "^3.6.0",
39	    "zod": "^4.4.3"
40	  },
41	  "devDependencies": {
42	    "@clerk/testing": "^2.0.27",
43	    "@eslint/compat": "^2.1.0",
44	    "@tailwindcss/postcss": "^4.3.0",
45	    "@testing-library/dom": "^10.4.1",
46	    "@testing-library/jest-dom": "^6.9.1",
47	    "@testing-library/react": "^16.3.2",
48	    "@testing-library/user-event": "^14.6.1",
49	    "@types/node": "^25.7.0",
50	    "@types/react": "^19.2.14",
51	    "@types/react-dom": "^19.2.3",
52	    "@typescript-eslint/parser": "^8.59.3",
53	    "@vitejs/plugin-react": "^6.0.1",
54	    "@vitest/coverage-v8": "^4.1.6",
55	    "concurrently": "^9.2.1",
56	    "dotenv-cli": "^11.0.0",
57	    "eslint": "^10.3.0",
58	    "eslint-config-next": "^16.2.6",
59	    "eslint-plugin-react": "^7.37.5",
60	    "eslint-plugin-react-hooks": "^7.1.1",
61	    "jsdom": "^29.1.1",
62	    "postcss": "^8.5.14",
63	    "prisma": "^7.8.0",
64	    "tailwindcss": "^4.3.0",
65	    "typescript": "^6.0.3",
66	    "typescript-eslint": "^8.59.3",
67	    "vitest": "^4.1.6"
68	  },
69	  "engines": {
70	    "node": ">=24.0.0"
71	  },
72	  "overrides": {
73	    "axios": ">=1.15.2",
74	    "hono": "^4.12.18",
75	    "lodash": ">=4.17.23",
76	    "minimatch": "10.2.4",
77	    "@hono/node-server": ">=1.19.13",
78	    "fast-uri": "^3.1.1"
79	  }
80	}
81

churn-renewal-forecast

concurrently "bun run dev" "bun run worker:dev" --names "dev,worker" --prefix-colors "cyan,magenta"


1	import { getBoss, closeBoss } from "@/lib/jobs/boss";
2	import { logger } from "@/lib/logger";
3	import { getApiTokenCount } from "@/lib/front";
4	import { handleSyncFrontAccounts } from "./handlers/sync-front-accounts";
5	import { handleSyncFrontContacts } from "./handlers/sync-front-contacts";
6	import { handleSyncFrontAccountContacts } from "./handlers/sync-front-account-contacts";
7	import { handleSyncFrontConversations } from "./handlers/sync-front-conversations";
8	import { handleSyncFrontAccountConversations } from "./handlers/sync-front-account-conversations";
9	import { handleSyncProductboardCompanies } from "./handlers/sync-productboard-companies";
10	import { handleSyncProductboardNotes } from "./handlers/sync-productboard-notes";
11	import { handleSyncProductboardFeatures } from "./handlers/sync-productboard-features";
12	import { handleSyncFrontCustomerConversations } from "./handlers/sync-front-customer-conversations";
13	import { handleSyncProductboardCustomerNotes } from "./handlers/sync-productboard-customer-notes";
14	import { handleImportConsumption } from "./handlers/import-consumption";
15	import { handleDetectUsageDrops } from "./handlers/detect-usage-drops";
16	import { cleanupStaleJobs } from "@/lib/jobs/cleanup";
17
18	async function startWorker() {
19	  try {
20	    logger.log("Starting pg-boss worker...");
21	    const boss = await getBoss();
22
23	    // Clean up any stale jobs from previous runs
24	    logger.log("Cleaning up stale jobs...");
25	    await cleanupStaleJobs();
26
27	    // Create queues before registering workers
28	    const queues = [
29	      "sync-front-accounts",
30	      "sync-front-contacts",
31	      "sync-front-account-contacts",
32	      "sync-front-conversations",
33	      "sync-front-account-conversations",
34	      "sync-front-customer-conversations",
35	      "sync-productboard-companies",
36	      "sync-productboard-notes",
37	      "sync-productboard-customer-notes",
38	      "sync-productboard-features",
39	      "import-consumption",
40	      "detect-usage-drops",
41	    ];
42
43	    logger.log("Creating job queues...");
44	    for (const queue of queues) {
45	      await boss.createQueue(queue);
46	    }
47
48	    // Register job handlers
49	    await boss.work("sync-front-accounts", handleSyncFrontAccounts);
50	    await boss.work("sync-front-contacts", handleSyncFrontContacts);
51	    await boss.work("sync-front-conversations", handleSyncFrontConversations);
52
53	    // Register sub-job handlers with concurrency options
54	    const frontConcurrency = 3 * Math.max(1, getApiTokenCount());
55	    logger.log(`[Worker] Front sub-job concurrency: ${frontConcurrency} (${Math.max(1, getApiTokenCount())} API key(s))`);
56
57	    await boss.work(
58	      "sync-front-account-contacts",
59	      {
60	        localConcurrency: frontConcurrency,
61	      },
62	      handleSyncFrontAccountContacts
63	    );
64
65	    await boss.work(
66	      "sync-front-account-conversations",
67	      {
68	        localConcurrency: frontConcurrency,
69	      },
70	      handleSyncFrontAccountConversations
71	    );
72
73	    await boss.work("sync-productboard-companies", handleSyncProductboardCompanies);
74	    await boss.work("sync-productboard-notes", handleSyncProductboardNotes);
75	    await boss.work("sync-productboard-features", handleSyncProductboardFeatures);
76	    await boss.work("sync-front-customer-conversations", handleSyncFrontCustomerConversations);
77	    await boss.work("sync-productboard-customer-notes", handleSyncProductboardCustomerNotes);
78	    await boss.work("import-consumption", handleImportConsumption);
79	    await boss.work("detect-usage-drops", handleDetectUsageDrops);
80
81	    logger.log("✓ Worker started successfully, listening for jobs...");
82
83	    // Handle graceful shutdown
84	    process.on("SIGINT", async () => {
85	      logger.log("\nShutting down worker...");
86	      await closeBoss();
87	      process.exit(0);
88	    });
89
90	    process.on("SIGTERM", async () => {
91	      logger.log("\nShutting down worker...");
92	      await closeBoss();
93	      process.exit(0);
94	    });
95	  } catch (error) {
96	    console.error("Failed to start worker:", error);
97	    process.exit(1);
98	  }
99	}
100
101	startWorker();
102

import { getBoss, closeBoss } from "@/lib/jobs/boss";
import { logger } from "@/lib/logger";
import { getApiTokenCount } from "@/lib/front";
import { handleSyncFrontAccounts } from "./handlers/sync-front-accounts";
import { handleSyncFrontContacts } from "./handlers/sync-front-contacts";
import { handleSyncFrontAccountContacts } from "./handlers/sync-front-account-contacts";
import { handleSyncFrontConversations } from "./handlers/sync-front-conversations";
import { handleSyncFrontAccountConversations } from "./handlers/sync-front-account-conversations";
import { handleSyncProductboardCompanies } from "./handlers/sync-productboard-companies";
import { handleSyncProductboardNotes } from "./handlers/sync-productboard-notes";
import { handleSyncProductboardFeatures } from "./handlers/sync-productboard-features";
import { handleSyncFrontCustomerConversations } from "./handlers/sync-front-customer-conversations";
import { handleSyncProductboardCustomerNotes } from "./handlers/sync-productboard-customer-notes";
import { handleImportConsumption } from "./handlers/import-consumption";
import { handleDetectUsageDrops } from "./handlers/detect-usage-drops";
import { cleanupStaleJobs } from "@/lib/jobs/cleanup";

async function startWorker() {
  try {
    logger.log("Starting pg-boss worker...");
    const boss = await getBoss();

    // Clean up any stale jobs from previous runs
    logger.log("Cleaning up stale jobs...");
    await cleanupStaleJobs();

    // Create queues before registering workers
    const queues = [
      "sync-front-accounts",
      "sync-front-contacts",
      "sync-front-account-contacts",
      "sync-front-conversations",
      "sync-front-account-conversations",
      "sync-front-customer-conversations",
      "sync-productboard-companies",
      "sync-productboard-notes",
      "sync-productboard-customer-notes",
      "sync-productboard-features",
      "import-consumption",
      "detect-usage-drops",
    ];

    logger.log("Creating job queues...");
    for (const queue of queues) {
      await boss.createQueue(queue);
    }

    // Register job handlers
    await boss.work("sync-front-accounts", handleSyncFrontAccounts);
    await boss.work("sync-front-contacts", handleSyncFrontContacts);
    await boss.work("sync-front-conversations", handleSyncFrontConversations);

    // Register sub-job handlers with concurrency options
    const frontConcurrency = 3 * Math.max(1, getApiTokenCount());
    logger.log(`[Worker] Front sub-job concurrency: ${frontConcurrency} (${Math.max(1, getApiTokenCount())} API key(s))`);

    await boss.work(
      "sync-front-account-contacts",
      {
        localConcurrency: frontConcurrency,
      },
      handleSyncFrontAccountContacts
    );

    await boss.work(
      "sync-front-account-conversations",
      {
        localConcurrency: frontConcurrency,
      },
      handleSyncFrontAccountConversations
    );

    await boss.work("sync-productboard-companies", handleSyncProductboardCompanies);
    await boss.work("sync-productboard-notes", handleSyncProductboardNotes);
    await boss.work("sync-productboard-features", handleSyncProductboardFeatures);
    await boss.work("sync-front-customer-conversations", handleSyncFrontCustomerConversations);
    await boss.work("sync-productboard-customer-notes", handleSyncProductboardCustomerNotes);
    await boss.work("import-consumption", handleImportConsumption);
    await boss.work("detect-usage-drops", handleDetectUsageDrops);

    logger.log("✓ Worker started successfully, listening for jobs...");

    // Handle graceful shutdown
    process.on("SIGINT", async () => {
      logger.log("\nShutting down worker...");
      await closeBoss();
      process.exit(0);
    });

    process.on("SIGTERM", async () => {
      logger.log("\nShutting down worker...");
      await closeBoss();
      process.exit(0);
    });
  } catch (error) {
    console.error("Failed to start worker:", error);
    process.exit(1);
  }
}

startWorker();


Good - I have full context now. No existing deploy/, worker runs `bunx tsx src/worker/index.ts`, web runs `next start`. Let me move to clarifying questions.


Updated task #2 status


Aight Sir Charles, context is bussin - got everything I need from the codebase fr fr. One question at a time before we start writing YAML:

**Will the web and worker share a single container image** (same image, different `command` overrides), or do they need separate images built from separate Dockerfiles?

This matters for how the Deployments reference images and whether the `init/` migration Job uses the same image.


afplay ~/.claude/sounds/mario/stop/stomp.wav 2>/dev/null || true


yes, same image but different command overrides


Bet. Next question:

**What's the container image name and registry?** For example: `ghcr.io/immuta/churn-renewal-forecast` or something like `123456789.dkr.ecr.us-east-1.amazonaws.com/churn-renewal-forecast`?

If you want I can use a placeholder like `REGISTRY/churn-renewal-forecast` and you can swap it in — just want to know what pattern to follow.


use a placeholder, i will be using our private docker registry that is backed by harbor (registry.ic-ops.net)


No cap, `registry.ic-ops.net/churn-renewal-forecast` it is. Next:

**What's the ExternalSecret SecretStore name and kind?** The `secret.yaml` ExternalSecret needs a `secretStoreRef`. Is it a `ClusterSecretStore` (cluster-scoped) or a namespace-scoped `SecretStore`, and what's it called — e.g., `vault-cluster-store`, `cluster-secret-store`, something else?


the externalsecret secret store is a clustersecretstore named vault


Last one before we design — I promise:

**What's the IAM role ARN for the EKS service account annotation** in the overlay? Something like `arn:aws:iam::123456789012:role/churn-renewal-forecast` — or should I use a placeholder there too?


use a placeholder for now because i don't think it needs an IAM role?


Good call - if it's not accessing AWS services directly (no S3, Secrets Manager etc.), IRSA isn't needed. I'll include it as a commented-out placeholder to match the saas-tenant-manager pattern but note it can be dropped.

I have everything I need. Let me present the design.

---


Updated task #3 status


## Approaches

**Option A — Base + single cluster overlay (recommended)** — exactly what you specified: a `base/` with reusable resources and a `deploy/eks-shared-infra-use1-2/` overlay that patches cluster-specific values (ingress visibility, SA annotation). Clean, minimal, matches the saas-tenant-manager pattern. This is the right call.

**Option B — Base + per-environment overlays (dev/staging/prod)** — useful if you have multiple environments. Overkill here; you have one cluster and one target environment.

**Option C — Helm chart** — more powerful templating but entirely different toolchain from what you asked for. Not considered further.

**Going with Option A.**

---

## Design

### Directory structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret → ClusterSecretStore "vault"
│   │   └── app-config.env        # ConfigMap source
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy (one-shot Job)
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml        # UDP 53 egress
│   │   ├── allow-rds.yaml        # TCP 5432 egress
│   │   └── allow-external.yaml  # TCP 443 egress to clerk/front/productboard/slack
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml    # private visibility annotation
        └── serviceaccount.yaml  # IRSA annotation (commented out)
```

### Key decisions

- **Single image `registry.ic-ops.net/churn-renewal-forecast:latest`** — web and worker use the same image with different `command` overrides. The init Job uses it too for `bunx prisma migrate deploy`.
- **Secret split** — Vault-sourced secrets (DATABASE_URL, CLERK_SECRET_KEY, FRONT_API_TOKEN, FRONT_API_TOKEN_2, PRODUCTBOARD_API_KEY, SLACK_BOT_TOKEN) live in the ExternalSecret. Non-sensitive config (APP_URL, NEXT_PUBLIC_* vars, pool tuning, SLACK_TEST_CHANNEL) lives in the ConfigMap via `app-config.env`. Both are mounted via `envFrom` on web and worker.
- **Network policies** — Three CiliumNetworkPolicies: DNS (UDP 53 to kube-dns), RDS (TCP 5432), and external egress (TCP 443) with `toFQDNs` entries for `clerk.com`, `api.frontapp.com`, `api.productboard.com`, `slack.com`.
- **Probes** — Web deployment gets liveness on `/api/health/live` and readiness on `/api/health/ready`, both HTTP on port 3000. Worker gets no HTTP probes (it's a queue consumer — liveness would just check the process is alive).
- **Resources** — web: `requests: 256Mi/100m`, `limits: 512Mi/500m`. Worker: same defaults.
- **Replicas** — 1 replica each for web and worker (you can patch this in the overlay).
- **Init Job** — `restartPolicy: OnFailure`, runs before app pods via the dependency ordering in the overlay's kustomization. The job is idempotent (`prisma migrate deploy` is safe to re-run).

Does this look right so far? Any adjustments before I write the spec and files?


this seems right yes


Slay, Sir Charles. Writing the spec doc now.


Updated task #4 status


Bash

Create specs directory


(Bash completed with no output)


Write

# Kustomize Deployment Design

**Date:** 2026-05-18
**Project:** churn-renewal-forecast
**Status:** Approved

## Overview

Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.

## Directory Structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
│   │   └── app-config.env        # ConfigMap values
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml
│   │   ├── allow-rds.yaml
│   │   └── allow-external.yaml
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml
        └── serviceaccount.yaml
```

## Components

### bootstrap/

**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.

**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).

**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:

| Vault key | Secret env key |
|---|---|
| `DATABASE_URL` | `DATABASE_URL` |
| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |

Refresh interval: `1h`.

**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:

| Key | Value |
|---|---|
| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
| `SLACK_TEST_CHANNEL` | `""` |
| `PG_POOL_MAX` | `10` |
| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |

### init/

**job.yaml** — One-shot Kubernetes Job:
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command: `["bunx", "prisma", "migrate", "deploy"]`
- `restartPolicy: OnFailure`
- `backoffLimit: 3`
- `envFrom` the ConfigMap and Secret
- Uses the `churn-renewal-forecast` ServiceAccount
- `ttlSecondsAfterFinished: 300` to clean up completed jobs

### netpol/

Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:

**allow-dns.yaml** — Egress to kube-dns on UDP port 53.

**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).

**allow-external.yaml** — Egress on TCP port 443 to:
- `clerk.com`
- `api.frontapp.com`
- `api.productboard.com`
- `slack.com`

Using `toFQDNs` matchers.

### services/

**web-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-web`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bun", "run", "start"]` (`next start`)
- Port: `3000`
- `envFrom` ConfigMap + Secret
- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
- ServiceAccount: `churn-renewal-forecast`

**worker-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-worker`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
- No HTTP port exposed
- `envFrom` ConfigMap + Secret
- Resources: same as web
- No HTTP probes (queue consumer; process liveness is sufficient)
- ServiceAccount: `churn-renewal-forecast`

**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.

**ingress.yaml** — Nginx Ingress:
- Host: `churn-forecast.infrastructure.immuta.io`
- Backend: the ClusterIP service on port 3000
- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)

### Overlay: deploy/eks-shared-infra-use1-2/

**kustomization.yaml** — References `../../base` and applies patches.

**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).

**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.

## Labels

All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.

## Image Strategy

Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.

## Open Items

- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.


File created successfully at: /Users/steve.briggs/projects/churn-renewal-forecast/docs/superpowers/specs/2026-05-18-kustomize-deployment-design.md (file state is current in your context — no need to Read it back)

# Kustomize Deployment Design

**Date:** 2026-05-18
**Project:** churn-renewal-forecast
**Status:** Approved

## Overview

Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.

## Directory Structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
│   │   └── app-config.env        # ConfigMap values
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml
│   │   ├── allow-rds.yaml
│   │   └── allow-external.yaml
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml
        └── serviceaccount.yaml
```

## Components

### bootstrap/

**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.

**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).

**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:

| Vault key | Secret env key |
|---|---|
| `DATABASE_URL` | `DATABASE_URL` |
| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |

Refresh interval: `1h`.

**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:

| Key | Value |
|---|---|
| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
| `SLACK_TEST_CHANNEL` | `""` |
| `PG_POOL_MAX` | `10` |
| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |

### init/

**job.yaml** — One-shot Kubernetes Job:
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command: `["bunx", "prisma", "migrate", "deploy"]`
- `restartPolicy: OnFailure`
- `backoffLimit: 3`
- `envFrom` the ConfigMap and Secret
- Uses the `churn-renewal-forecast` ServiceAccount
- `ttlSecondsAfterFinished: 300` to clean up completed jobs

### netpol/

Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:

**allow-dns.yaml** — Egress to kube-dns on UDP port 53.

**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).

**allow-external.yaml** — Egress on TCP port 443 to:
- `clerk.com`
- `api.frontapp.com`
- `api.productboard.com`
- `slack.com`

Using `toFQDNs` matchers.

### services/

**web-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-web`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bun", "run", "start"]` (`next start`)
- Port: `3000`
- `envFrom` ConfigMap + Secret
- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
- ServiceAccount: `churn-renewal-forecast`

**worker-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-worker`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
- No HTTP port exposed
- `envFrom` ConfigMap + Secret
- Resources: same as web
- No HTTP probes (queue consumer; process liveness is sufficient)
- ServiceAccount: `churn-renewal-forecast`

**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.

**ingress.yaml** — Nginx Ingress:
- Host: `churn-forecast.infrastructure.immuta.io`
- Backend: the ClusterIP service on port 3000
- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)

### Overlay: deploy/eks-shared-infra-use1-2/

**kustomization.yaml** — References `../../base` and applies patches.

**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).

**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.

## Labels

All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.

## Image Strategy

Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.

## Open Items

- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.


**Spec self-review:**

- No TBDs or contradictions found
- NEXT_PUBLIC placeholders and FRONT_RATE_LIMIT_RPM are captured in Open Items
- One gap: Kustomize doesn't enforce init Job → Deployment ordering natively — that's an ArgoCD sync-wave concern. Adding that note.


Edit

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:
- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)
- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.

## Open Items


The file /Users/steve.briggs/projects/churn-renewal-forecast/docs/superpowers/specs/2026-05-18-kustomize-deployment-design.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:
- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)
- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.

## Open Items

# Kustomize Deployment Design

**Date:** 2026-05-18
**Project:** churn-renewal-forecast
**Status:** Approved

## Overview

Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.

## Directory Structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
│   │   └── app-config.env        # ConfigMap values
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml
│   │   ├── allow-rds.yaml
│   │   └── allow-external.yaml
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml
        └── serviceaccount.yaml
```

## Components

### bootstrap/

**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.

**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).

**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:

| Vault key | Secret env key |
|---|---|
| `DATABASE_URL` | `DATABASE_URL` |
| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |

Refresh interval: `1h`.

**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:

| Key | Value |
|---|---|
| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
| `SLACK_TEST_CHANNEL` | `""` |
| `PG_POOL_MAX` | `10` |
| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |

### init/

**job.yaml** — One-shot Kubernetes Job:
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command: `["bunx", "prisma", "migrate", "deploy"]`
- `restartPolicy: OnFailure`
- `backoffLimit: 3`
- `envFrom` the ConfigMap and Secret
- Uses the `churn-renewal-forecast` ServiceAccount
- `ttlSecondsAfterFinished: 300` to clean up completed jobs

### netpol/

Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:

**allow-dns.yaml** — Egress to kube-dns on UDP port 53.

**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).

**allow-external.yaml** — Egress on TCP port 443 to:
- `clerk.com`
- `api.frontapp.com`
- `api.productboard.com`
- `slack.com`

Using `toFQDNs` matchers.

### services/

**web-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-web`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bun", "run", "start"]` (`next start`)
- Port: `3000`
- `envFrom` ConfigMap + Secret
- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
- ServiceAccount: `churn-renewal-forecast`

**worker-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-worker`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
- No HTTP port exposed
- `envFrom` ConfigMap + Secret
- Resources: same as web
- No HTTP probes (queue consumer; process liveness is sufficient)
- ServiceAccount: `churn-renewal-forecast`

**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.

**ingress.yaml** — Nginx Ingress:
- Host: `churn-forecast.infrastructure.immuta.io`
- Backend: the ClusterIP service on port 3000
- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)

### Overlay: deploy/eks-shared-infra-use1-2/

**kustomization.yaml** — References `../../base` and applies patches.

**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).

**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.

## Labels

All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.

## Image Strategy

Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.

## Open Items

- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.

+Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:

+- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)

+- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

+The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.


Bash

git add docs/superpowers/specs/2026-05-18-kustomize-deployment-design.md && git commit -m "$(cat <<'EOF'
docs: add kustomize deployment design spec no cap

spec for the deploy/kubernetes/ directory structure,
base + eks-shared-infra-use1-2 overlay pattern bussin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Commit the design spec


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.


[Request interrupted by user for tool use]


do not add argocd annotations, we are not using argocd anymore -- only flux


Bet, updating the spec to drop ArgoCD references.


Edit

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:
- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)
- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering is handled via Flux `Kustomization` resources with `dependsOn` — the init kustomization must complete (health check: Job succeeded) before the services kustomization is applied.

The overlay's `.app.yaml` captures the Flux app metadata for this pattern.


## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering is handled via Flux `Kustomization` resources with `dependsOn` — the init kustomization must complete (health check: Job succeeded) before the services kustomization is applied.

The overlay's `.app.yaml` captures the Flux app metadata for this pattern.

# Kustomize Deployment Design

**Date:** 2026-05-18
**Project:** churn-renewal-forecast
**Status:** Approved

## Overview

Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.

## Directory Structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
│   │   └── app-config.env        # ConfigMap values
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml
│   │   ├── allow-rds.yaml
│   │   └── allow-external.yaml
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml
        └── serviceaccount.yaml
```

## Components

### bootstrap/

**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.

**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).

**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:

| Vault key | Secret env key |
|---|---|
| `DATABASE_URL` | `DATABASE_URL` |
| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |

Refresh interval: `1h`.

**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:

| Key | Value |
|---|---|
| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
| `SLACK_TEST_CHANNEL` | `""` |
| `PG_POOL_MAX` | `10` |
| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |

### init/

**job.yaml** — One-shot Kubernetes Job:
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command: `["bunx", "prisma", "migrate", "deploy"]`
- `restartPolicy: OnFailure`
- `backoffLimit: 3`
- `envFrom` the ConfigMap and Secret
- Uses the `churn-renewal-forecast` ServiceAccount
- `ttlSecondsAfterFinished: 300` to clean up completed jobs

### netpol/

Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:

**allow-dns.yaml** — Egress to kube-dns on UDP port 53.

**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).

**allow-external.yaml** — Egress on TCP port 443 to:
- `clerk.com`
- `api.frontapp.com`
- `api.productboard.com`
- `slack.com`

Using `toFQDNs` matchers.

### services/

**web-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-web`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bun", "run", "start"]` (`next start`)
- Port: `3000`
- `envFrom` ConfigMap + Secret
- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
- ServiceAccount: `churn-renewal-forecast`

**worker-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-worker`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
- No HTTP port exposed
- `envFrom` ConfigMap + Secret
- Resources: same as web
- No HTTP probes (queue consumer; process liveness is sufficient)
- ServiceAccount: `churn-renewal-forecast`

**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.

**ingress.yaml** — Nginx Ingress:
- Host: `churn-forecast.infrastructure.immuta.io`
- Backend: the ClusterIP service on port 3000
- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)

### Overlay: deploy/eks-shared-infra-use1-2/

**kustomization.yaml** — References `../../base` and applies patches.

**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).

**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.

## Labels

All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.

## Image Strategy

Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:
- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)
- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.

## Open Items

- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.

-Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering must be handled at the deployment-tool level:

-- **ArgoCD** — add `argocd.argoproj.io/sync-wave` annotations (init Job at wave `-1`, Deployments at wave `0`)

-- **Flux** — use a `Kustomization` with `dependsOn` referencing the init job kustomization

+Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering is handled via Flux `Kustomization` resources with `dependsOn` — the init kustomization must complete (health check: Job succeeded) before the services kustomization is applied.

-The overlay's `.app.yaml` should include whatever sync-wave or dependency config the cluster uses.

+The overlay's `.app.yaml` captures the Flux app metadata for this pattern.


Also dropping the ArgoCD mention from the `.app.yaml` description in the overlay section:


**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**.app.yaml** — Flux app metadata file with app name, namespace, and target cluster.


# Kustomize Deployment Design

**Date:** 2026-05-18
**Project:** churn-renewal-forecast
**Status:** Approved

## Overview

Add a Kustomize-based Kubernetes deployment under `deploy/kubernetes/`, modelled after the `saas-tenant-manager` pattern. The app has two runtime components (Next.js web server and pg-boss background worker) that share a single container image with different command overrides. A one-shot init Job handles Prisma migrations before pods come up.

## Directory Structure

```
deploy/kubernetes/
├── base/
│   ├── kustomization.yaml
│   ├── bootstrap/
│   │   ├── kustomization.yaml
│   │   ├── namespace.yaml
│   │   ├── serviceaccount.yaml
│   │   ├── secret.yaml           # ExternalSecret pulling from Vault
│   │   └── app-config.env        # ConfigMap values
│   ├── init/
│   │   ├── kustomization.yaml
│   │   └── job.yaml              # Prisma migrate deploy
│   ├── netpol/
│   │   ├── kustomization.yaml
│   │   ├── allow-dns.yaml
│   │   ├── allow-rds.yaml
│   │   └── allow-external.yaml
│   └── services/
│       ├── kustomization.yaml
│       ├── web-deployment.yaml
│       ├── worker-deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
└── deploy/
    └── eks-shared-infra-use1-2/
        ├── kustomization.yaml
        ├── .app.yaml
        ├── ingress-patch.yaml
        └── serviceaccount.yaml
```

## Components

### bootstrap/

**namespace.yaml** — Creates the `churn-renewal-forecast` namespace with label `app.kubernetes.io/name: churn-renewal-forecast`.

**serviceaccount.yaml** — ServiceAccount named `churn-renewal-forecast` in the namespace. No IRSA annotation needed at base level (app does not access AWS APIs directly).

**secret.yaml** — `ExternalSecret` (external-secrets.io v1beta1) that pulls from `ClusterSecretStore` named `vault` at path `/internal/churn-renewal-forecast`. Maps the following keys to a Secret named `churn-renewal-forecast`:

| Vault key | Secret env key |
|---|---|
| `DATABASE_URL` | `DATABASE_URL` |
| `CLERK_SECRET_KEY` | `CLERK_SECRET_KEY` |
| `FRONT_API_TOKEN` | `FRONT_API_TOKEN` |
| `FRONT_API_TOKEN_2` | `FRONT_API_TOKEN_2` |
| `PRODUCTBOARD_API_KEY` | `PRODUCTBOARD_API_KEY` |
| `SLACK_BOT_TOKEN` | `SLACK_BOT_TOKEN` |

Refresh interval: `1h`.

**app-config.env** — ConfigMap (generated by Kustomize `configMapGenerator`) with non-sensitive config:

| Key | Value |
|---|---|
| `APP_URL` | `https://churn-forecast.infrastructure.immuta.io` |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `PLACEHOLDER` |
| `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` | `PLACEHOLDER` |
| `SLACK_TEST_CHANNEL` | `""` |
| `PG_POOL_MAX` | `10` |
| `PG_POOL_IDLE_TIMEOUT_MS` | `30000` |
| `PG_POOL_CONNECTION_TIMEOUT_MS` | `2000` |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | `/` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | `/` |

### init/

**job.yaml** — One-shot Kubernetes Job:
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command: `["bunx", "prisma", "migrate", "deploy"]`
- `restartPolicy: OnFailure`
- `backoffLimit: 3`
- `envFrom` the ConfigMap and Secret
- Uses the `churn-renewal-forecast` ServiceAccount
- `ttlSecondsAfterFinished: 300` to clean up completed jobs

### netpol/

Three `CiliumNetworkPolicy` resources, all selecting pods with `app.kubernetes.io/name: churn-renewal-forecast`:

**allow-dns.yaml** — Egress to kube-dns on UDP port 53.

**allow-rds.yaml** — Egress on TCP port 5432 (PostgreSQL/RDS).

**allow-external.yaml** — Egress on TCP port 443 to:
- `clerk.com`
- `api.frontapp.com`
- `api.productboard.com`
- `slack.com`

Using `toFQDNs` matchers.

### services/

**web-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-web`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bun", "run", "start"]` (`next start`)
- Port: `3000`
- `envFrom` ConfigMap + Secret
- Resources: `requests: {memory: 256Mi, cpu: 100m}`, `limits: {memory: 512Mi, cpu: 500m}`
- Liveness probe: HTTP GET `/api/health/live` port 3000, `initialDelaySeconds: 15`, `periodSeconds: 20`
- Readiness probe: HTTP GET `/api/health/ready` port 3000, `initialDelaySeconds: 10`, `periodSeconds: 10`
- ServiceAccount: `churn-renewal-forecast`

**worker-deployment.yaml** — Deployment:
- Name: `churn-renewal-forecast-worker`
- Replicas: `1`
- Image: `registry.ic-ops.net/churn-renewal-forecast:latest`
- Command override: `["bunx", "tsx", "src/worker/index.ts"]`
- No HTTP port exposed
- `envFrom` ConfigMap + Secret
- Resources: same as web
- No HTTP probes (queue consumer; process liveness is sufficient)
- ServiceAccount: `churn-renewal-forecast`

**service.yaml** — ClusterIP Service targeting `churn-renewal-forecast-web` pods on port 3000.

**ingress.yaml** — Nginx Ingress:
- Host: `churn-forecast.infrastructure.immuta.io`
- Backend: the ClusterIP service on port 3000
- TLS: references a secret `churn-renewal-forecast-tls` (managed externally / cert-manager)

### Overlay: deploy/eks-shared-infra-use1-2/

**kustomization.yaml** — References `../../base` and applies patches.

**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

**ingress-patch.yaml** — Strategic merge patch adding the private visibility annotation to the base Ingress (e.g., `nginx.ingress.kubernetes.io/scheme: internal` or equivalent annotation used by the cluster).

**serviceaccount.yaml** — Patch file. IRSA annotation is commented out since no AWS API access is needed. Can be uncommented if requirements change.

## Labels

All resources use `app.kubernetes.io/name: churn-renewal-forecast` consistently. Deployments also add `app.kubernetes.io/component: web` or `app.kubernetes.io/component: worker`.

## Image Strategy

Single image `registry.ic-ops.net/churn-renewal-forecast:latest` shared across web Deployment, worker Deployment, and init Job. The image tag should be pinned to a specific digest or semver tag in CI via a Kustomize image transform in the overlay.

## Deployment Ordering

Kustomize itself does not enforce that the init Job completes before the web/worker Deployments start. Ordering is handled via Flux `Kustomization` resources with `dependsOn` — the init kustomization must complete (health check: Job succeeded) before the services kustomization is applied.

The overlay's `.app.yaml` captures the Flux app metadata for this pattern.

## Open Items

- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NEXT_PUBLIC_PRODUCTBOARD_WORKSPACE` values need to be filled in for the target environment.
- TLS secret name (`churn-renewal-forecast-tls`) assumes cert-manager or equivalent; adjust if cluster uses a different mechanism.
- Confirm whether `FRONT_RATE_LIMIT_RPM` should be in the ConfigMap (currently omitted since the spec didn't list it; add if needed).
- If the app ever needs AWS API access (S3, Secrets Manager), uncomment the IRSA annotation in the overlay serviceaccount.yaml and supply the role ARN.

-**.app.yaml** — Metadata file (ArgoCD / Flux app descriptor) with app name, namespace, and target cluster.

+**.app.yaml** — Flux app metadata file with app name, namespace, and target cluster.


Bash

git add docs/superpowers/specs/2026-05-18-kustomize-deployment-design.md && git commit -m "$(cat <<'EOF'
docs: add kustomize deployment design spec no cap

spec for the deploy/kubernetes/ directory structure,
base + eks-shared-infra-use1-2 overlay pattern bussin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Commit the design spec


don't add design docs to git


Explore project context

Check existing deploy/ directory, docs, and project structure to understand current patterns

Ask clarifying questions

One at a time, understand any gaps in the spec before designing

Propose approaches and present design

Present the Kustomize structure design for approval

Write design doc

Save spec to docs/superpowers/specs/YYYY-MM-DD-kustomize-deployment-design.md and commit

Transition to writing-plans

Invoke writing-plans skill to create implementation plan


Got it, no git commits for docs. Spec is written at `docs/superpowers/specs/2026-05-18-kustomize-deployment-design.md` — please review it and let me know if you want any changes before I move to the implementation plan.


can we write the design docs to obsidian? and can we also set claude to automatically write the superpowers design docs to obsidian as well as the local project directory?


Two things happening here — saving to Obsidian now, and configuring the automation. Let me invoke both skills.


Launching skill: obsidian-save