{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://flui.cloud/schema/catalog-app/v1beta1.json",
  "title": "Flui Catalog App Manifest (v1beta1)",
  "type": "object",
  "required": [
    "kind",
    "apiVersion",
    "metadata",
    "spec"
  ],
  "additionalProperties": false,
  "properties": {
    "kind": {
      "const": "CatalogApp"
    },
    "apiVersion": {
      "enum": [
        "flui.cloud/v1beta1",
        "flui/v1"
      ]
    },
    "metadata": {
      "$ref": "#/definitions/metadata"
    },
    "spec": {
      "$ref": "#/definitions/spec"
    }
  },
  "definitions": {
    "metadata": {
      "type": "object",
      "required": [
        "id",
        "name",
        "appKind",
        "category",
        "version"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]{0,63}$"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        },
        "description": {
          "type": "string"
        },
        "appKind": {
          "type": "string",
          "enum": [
            "DATABASE",
            "APPLICATION",
            "TOOL",
            "SYSTEM"
          ]
        },
        "category": {
          "type": "string",
          "minLength": 1
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "license": {
          "type": "string"
        },
        "version": {
          "type": "string",
          "minLength": 1
        },
        "icon": {
          "type": "string",
          "format": "uri"
        },
        "links": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "website": {
              "type": "string",
              "format": "uri"
            },
            "docs": {
              "type": "string",
              "format": "uri"
            },
            "source": {
              "type": "string",
              "format": "uri"
            }
          }
        },
        "ratings": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "wow": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            },
            "utility": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            },
            "euFit": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            },
            "community": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            }
          }
        },
        "alternativeTo": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "maintainedAt": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
          "description": "ISO date (YYYY-MM-DD) of the last time this manifest was reviewed or updated. Used by the dashboard to surface stale catalog entries."
        },
        "entrypointPath": {
          "type": "string",
          "pattern": "^/",
          "description": "Path to append to the app's base URL when the dashboard renders the 'Open app' button. Default '/'. Use this for apps whose root does not serve a UI (e.g. PocketBase where '/' returns 404 and '/_/' is the admin UI, or Jupyter notebooks on '/lab')."
        },
        "clientFor": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]{0,63}$"
          },
          "uniqueItems": true,
          "description": "Building-block slugs this app is a client/UI for. The dashboard surfaces the app under 'Compatible clients' on each listed BB's install page. Pairs with spec.linkedBuildingBlocks to describe how to wire env vars from each BB."
        },
        "clientDefaultFor": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]{0,63}$"
          },
          "uniqueItems": true,
          "description": "Subset of clientFor. Building-block slugs for which this app is the recommended/default client. GET /catalog/:slug/clients exposes a per-target isDefault flag derived from this list."
        },
        "draft": {
          "type": "boolean",
          "description": "When true, the manifest is seeded but kept unpublished — it does not appear in GET /catalog and cannot be installed by end users. Use for work-in-progress entries kept in the repo."
        }
      }
    },
    "spec": {
      "oneOf": [
        {
          "$ref": "#/definitions/specStandalone"
        },
        {
          "$ref": "#/definitions/specBuildingBlock"
        },
        {
          "$ref": "#/definitions/specComposed"
        }
      ]
    },
    "specStandalone": {
      "type": "object",
      "required": [
        "type",
        "image",
        "ports",
        "env",
        "resources",
        "scaling"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "standalone"
        },
        "image": {
          "$ref": "#/definitions/image"
        },
        "ports": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/port"
          }
        },
        "volumes": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/volume"
          }
        },
        "persistence": {
          "$ref": "#/definitions/persistence"
        },
        "env": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/envVar"
          }
        },
        "resources": {
          "$ref": "#/definitions/resources"
        },
        "scaling": {
          "$ref": "#/definitions/scaling"
        },
        "healthcheck": {
          "$ref": "#/definitions/healthcheck"
        },
        "exposure": {
          "$ref": "#/definitions/exposure"
        },
        "privatizable": {
          "type": "boolean",
          "default": true
        },
        "domain": {
          "$ref": "#/definitions/domain"
        },
        "auth": {
          "$ref": "#/definitions/auth"
        },
        "access": {
          "$ref": "#/definitions/access"
        },
        "postInstall": {
          "$ref": "#/definitions/postInstall"
        },
        "startCommand": {
          "type": "string"
        },
        "linkedBuildingBlocks": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/linkedBuildingBlock"
          }
        },
        "dependencies": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/dependency"
          }
        },
        "smokeTest": {
          "$ref": "#/definitions/smokeTest"
        }
      }
    },
    "specBuildingBlock": {
      "type": "object",
      "required": [
        "type",
        "image",
        "ports",
        "env",
        "resources",
        "scaling",
        "healthcheck"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "building-block"
        },
        "connection": {
          "type": "object",
          "required": [
            "url"
          ],
          "additionalProperties": false,
          "description": "How a client reaches this block. Declared once here so no client manifest has to restate a DSN it could get wrong.",
          "properties": {
            "url": {
              "type": "string",
              "description": "Connection URL template. `{{host}}` and `{{port}}` are the block's address, `{{env.NAME}}` one of its own env values — a secret one is resolved where the secret lives, never in a manifest or a plan."
            }
          }
        },
        "image": {
          "$ref": "#/definitions/image"
        },
        "ports": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/port"
          }
        },
        "volumes": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/volume"
          }
        },
        "persistence": {
          "$ref": "#/definitions/persistence"
        },
        "env": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/envVar"
          }
        },
        "resources": {
          "$ref": "#/definitions/resources"
        },
        "scaling": {
          "$ref": "#/definitions/scaling"
        },
        "healthcheck": {
          "$ref": "#/definitions/healthcheck"
        },
        "auth": {
          "$ref": "#/definitions/auth"
        },
        "access": {
          "$ref": "#/definitions/access"
        },
        "postInstall": {
          "$ref": "#/definitions/postInstall"
        },
        "startCommand": {
          "type": "string"
        },
        "smokeTest": {
          "$ref": "#/definitions/smokeTest"
        },
        "dependencies": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/dependency"
          }
        }
      }
    },
    "specComposed": {
      "type": "object",
      "required": [
        "type",
        "components"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "composed"
        },
        "scalingPolicy": {
          "$ref": "#/definitions/scalingPolicy"
        },
        "networking": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "internal": {
              "type": "string"
            }
          }
        },
        "domain": {
          "$ref": "#/definitions/domain"
        },
        "auth": {
          "$ref": "#/definitions/auth"
        },
        "access": {
          "$ref": "#/definitions/access"
        },
        "postInstall": {
          "$ref": "#/definitions/postInstall"
        },
        "components": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/component"
          }
        },
        "smokeTest": {
          "$ref": "#/definitions/smokeTest"
        },
        "options": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "key",
              "label"
            ],
            "additionalProperties": false,
            "properties": {
              "key": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "default": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    "component": {
      "type": "object",
      "required": [
        "name",
        "image",
        "env",
        "resources",
        "scaling"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]{0,63}$"
        },
        "image": {
          "$ref": "#/definitions/image"
        },
        "ports": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/port"
          }
        },
        "volumes": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/volume"
          }
        },
        "persistence": {
          "$ref": "#/definitions/persistence"
        },
        "env": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/envVar"
          }
        },
        "resources": {
          "$ref": "#/definitions/resources"
        },
        "scaling": {
          "$ref": "#/definitions/scaling"
        },
        "healthcheck": {
          "$ref": "#/definitions/healthcheck"
        },
        "dependsOn": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "startCommand": {
          "type": "string",
          "description": "Overrides the image CMD (run as `sh -c`). Use for self-initializing components: run idempotent setup (migrations, config) then `exec` the image's original command, so every boot converges to a ready state and a failed init fails the unit instead of serving a broken app."
        },
        "when": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "option": {
              "type": "string"
            }
          }
        }
      }
    },
    "image": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "registry": {
          "type": "string"
        },
        "repository": {
          "type": "string"
        },
        "tag": {
          "type": "string"
        },
        "credentials": {
          "type": "object",
          "required": [
            "type",
            "secretRef"
          ],
          "additionalProperties": false,
          "properties": {
            "type": {
              "enum": [
                "registry",
                "git-token"
              ]
            },
            "secretRef": {
              "type": "string"
            }
          }
        },
        "source": {
          "type": "object",
          "required": [
            "type",
            "url",
            "branch"
          ],
          "additionalProperties": false,
          "properties": {
            "type": {
              "const": "git"
            },
            "url": {
              "type": "string",
              "format": "uri"
            },
            "branch": {
              "type": "string"
            },
            "dockerfile": {
              "type": "string"
            }
          }
        }
      }
    },
    "port": {
      "type": "object",
      "required": [
        "name",
        "internal",
        "expose"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string"
        },
        "internal": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        },
        "expose": {
          "type": "boolean"
        },
        "protocol": {
          "enum": [
            "http",
            "https",
            "tcp"
          ],
          "description": "http: plain web port, routable by the ingress. https: the container terminates TLS itself (usually self-signed) and refuses or redirects plain HTTP — the ingress proxies to it over TLS without verifying the upstream certificate; use this when the image forces HTTPS internally (symptom on http: an unconditional 301 redirect loop behind the ingress). tcp: non-web port, never ingress-routed."
        }
      }
    },
    "volume": {
      "type": "object",
      "required": [
        "name",
        "mountPath"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string"
        },
        "mountPath": {
          "type": "string"
        },
        "required": {
          "type": "boolean"
        },
        "size": {
          "type": "string"
        }
      }
    },
    "persistence": {
      "type": "object",
      "required": [
        "scope"
      ],
      "additionalProperties": false,
      "properties": {
        "scope": {
          "type": "string",
          "enum": [
            "shared",
            "dedicated"
          ]
        }
      }
    },
    "envVar": {
      "type": "object",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
        },
        "value": {
          "type": "string"
        },
        "secret": {
          "type": "boolean",
          "description": "When true, the resolved value is stored in the component's Secret (encrypted at rest) and injected via secretKeyRef, never as a plaintext ConfigMap entry. Use it on any env carrying sensitive data — typically a `value` that references another component's secret (e.g. a database password). `valueFrom.generate` and a `userInput` marked `sensitive` are always treated as secrets regardless of this flag."
        },
        "valueFrom": {
          "$ref": "#/definitions/valueFrom"
        },
        "userEditable": {
          "type": "boolean"
        },
        "description": {
          "type": "string"
        }
      },
      "oneOf": [
        {
          "required": [
            "value"
          ],
          "not": {
            "required": [
              "valueFrom"
            ]
          }
        },
        {
          "required": [
            "valueFrom"
          ],
          "not": {
            "required": [
              "value"
            ]
          }
        }
      ]
    },
    "valueFrom": {
      "oneOf": [
        {
          "type": "object",
          "required": [
            "generate",
            "length"
          ],
          "additionalProperties": false,
          "properties": {
            "generate": {
              "const": "secret"
            },
            "length": {
              "type": "integer",
              "minimum": 8,
              "maximum": 256
            },
            "format": {
              "enum": [
                "base64url",
                "hex"
              ]
            }
          }
        },
        {
          "type": "object",
          "required": [
            "secretRef"
          ],
          "additionalProperties": false,
          "properties": {
            "secretRef": {
              "type": "string"
            }
          }
        },
        {
          "type": "object",
          "required": [
            "userInput"
          ],
          "additionalProperties": false,
          "properties": {
            "userInput": {
              "type": "object",
              "additionalProperties": false,
              "allOf": [
                {
                  "if": {
                    "required": [
                      "group"
                    ]
                  },
                  "then": {
                    "properties": {
                      "required": false,
                      "default": false
                    }
                  }
                }
              ],
              "properties": {
                "label": {
                  "type": "string"
                },
                "default": {
                  "type": "string"
                },
                "sensitive": {
                  "type": "boolean"
                },
                "required": {
                  "type": "boolean",
                  "description": "Whether the installer must collect a value for this input. Independent of `sensitive` (which only controls whether the value is stored encrypted as a Secret vs a plaintext env). Defaults to the value of `sensitive`: a sensitive input is required unless this is explicitly `false`. Set `false` on a sensitive input to make it optional (unset → the env is not injected), or `true` on a non-sensitive input to require it."
                },
                "group": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9-]{0,31}$",
                  "description": "Inputs sharing a group id form an 'at least one of' set: each member is individually optional, but the installer must collect a value for AT LEAST ONE member of the group (e.g. four LLM-provider keys where any one works). A group needs ≥2 members to be meaningful. Mutually exclusive with `required` and `default` (a group governs presence collectively, so an individual `required`/`default` would be contradictory)."
                },
                "placeholder": {
                  "type": "string"
                },
                "pattern": {
                  "type": "string",
                  "format": "regex"
                },
                "patternDescription": {
                  "type": "string"
                },
                "minLength": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 1024
                },
                "maxLength": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 4096
                },
                "confirm": {
                  "type": "boolean"
                },
                "format": {
                  "enum": [
                    "email",
                    "url",
                    "password",
                    "text"
                  ]
                }
              }
            }
          }
        }
      ]
    },
    "resources": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "requests": {
          "$ref": "#/definitions/resourceSpec"
        },
        "limits": {
          "$ref": "#/definitions/resourceSpec"
        }
      }
    },
    "resourceSpec": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "cpu": {
          "type": "string"
        },
        "memory": {
          "type": "string"
        }
      }
    },
    "scaling": {
      "type": "object",
      "required": [
        "horizontal",
        "vertical"
      ],
      "additionalProperties": false,
      "properties": {
        "horizontal": {
          "type": "object",
          "required": [
            "enabled"
          ],
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "min": {
              "type": "integer",
              "minimum": 0
            },
            "max": {
              "type": "integer",
              "minimum": 1
            },
            "metrics": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "type",
                  "target"
                ],
                "additionalProperties": false,
                "properties": {
                  "type": {
                    "enum": [
                      "cpu",
                      "memory",
                      "custom"
                    ]
                  },
                  "target": {
                    "type": "object",
                    "required": [
                      "type",
                      "value"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "type": {
                        "enum": [
                          "utilization",
                          "averageValue"
                        ]
                      },
                      "value": {
                        "type": "number",
                        "minimum": 0
                      }
                    }
                  }
                }
              }
            },
            "behavior": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "scaleUp": {
                  "$ref": "#/definitions/hpaBehaviorPolicy"
                },
                "scaleDown": {
                  "$ref": "#/definitions/hpaBehaviorPolicy"
                }
              }
            }
          }
        },
        "vertical": {
          "type": "object",
          "required": [
            "enabled"
          ],
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "mode": {
              "enum": [
                "Off",
                "Initial",
                "Recreate",
                "Auto"
              ]
            },
            "bounds": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "cpu": {
                  "$ref": "#/definitions/vpaRange"
                },
                "memory": {
                  "$ref": "#/definitions/vpaRange"
                }
              }
            },
            "updatePolicy": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "trigger": {
                  "type": "array",
                  "items": {
                    "enum": [
                      "OOMKilled",
                      "CPUThrottling"
                    ]
                  }
                },
                "cooldown": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "hpaBehaviorPolicy": {
      "type": "object",
      "required": [
        "stabilizationWindow",
        "step"
      ],
      "additionalProperties": false,
      "properties": {
        "stabilizationWindow": {
          "type": "string"
        },
        "step": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "vpaRange": {
      "type": "object",
      "required": [
        "min",
        "max"
      ],
      "additionalProperties": false,
      "properties": {
        "min": {
          "type": "string"
        },
        "max": {
          "type": "string"
        }
      }
    },
    "scalingPolicy": {
      "type": "object",
      "required": [
        "mode"
      ],
      "additionalProperties": false,
      "properties": {
        "mode": {
          "enum": [
            "conservative",
            "balanced",
            "aggressive"
          ]
        },
        "notifications": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "onScaleUp": {
              "type": "boolean"
            },
            "onOOMKill": {
              "type": "boolean"
            },
            "onScaleDown": {
              "type": "boolean"
            },
            "onVerticalResize": {
              "type": "boolean"
            }
          }
        }
      }
    },
    "healthcheck": {
      "type": "object",
      "additionalProperties": false,
      "description": "Container health probe. It is run by the runtime, not by the image: the deploy learns from it whether the application actually came up, and `application.status` reports what it answered. This definition is shared field for field with the other manifest kind — a parity test keeps the two deep-equal, so a probe written for one kind is valid on the other.",
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "http",
            "tcp",
            "exec"
          ],
          "default": "http",
          "description": "Probe kind. Omitting it means http, so a manifest that declares only `path` is complete."
        },
        "path": {
          "type": "string",
          "pattern": "^/",
          "description": "HTTP path probed for readiness/liveness. MUST be a real route that returns 2xx — a wrong path (e.g. /health when the route is /api/health) makes every probe fail silently. Required for an http probe."
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "description": "Port the probe connects to inside the container. Defaults to the container's exposed port (`deploy.port` for an Application)."
        },
        "command": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Argv of an `exec` probe, run inside the container. Required when type is exec. Use it when the app ships its own check and no HTTP route answers before it is ready."
        },
        "initialDelay": {
          "type": "string",
          "description": "Grace period before the first probe, as a duration (`30s`, `2m`). An application that migrates a database on first boot needs one; without it the first probe fails on a container that is merely still starting."
        },
        "interval": {
          "type": "string",
          "description": "Time between probes, as a duration (`10s`, `1m`)."
        },
        "timeout": {
          "type": "string",
          "description": "How long one probe may take before it counts as failed, as a duration (`3s`)."
        },
        "retries": {
          "type": "integer",
          "minimum": 1,
          "description": "Consecutive failures before the container is reported unhealthy."
        },
        "httpHeaders": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "Extra request headers for an http probe — a `Host` the framework trusts, or a header a protected route requires. Without it the probe arrives as a bare loopback request, which a Host-validating framework rejects."
        }
      },
      "allOf": [
        {
          "if": {
            "anyOf": [
              {
                "not": {
                  "required": [
                    "type"
                  ]
                }
              },
              {
                "required": [
                  "type"
                ],
                "properties": {
                  "type": {
                    "const": "http"
                  }
                }
              }
            ]
          },
          "then": {
            "required": [
              "path"
            ]
          }
        },
        {
          "if": {
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "const": "exec"
              }
            }
          },
          "then": {
            "required": [
              "command"
            ]
          }
        }
      ]
    },
    "exposure": {
      "type": "string",
      "enum": [
        "public",
        "internal"
      ],
      "default": "public",
      "description": "Controls how the app is reached. 'public' (default) generates Ingress + Certificate + DNS on a public hostname. 'internal' skips all public exposure: only Deployment + Service ClusterIP are created; the app is reachable only from the Flui dashboard via the ForwardAuth proxy on a wildcard internal hostname."
    },
    "domain": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "auto": {
          "type": "boolean"
        },
        "userCustomizable": {
          "type": "boolean"
        },
        "tls": {
          "type": "boolean"
        },
        "hostnameMode": {
          "type": "string",
          "enum": [
            "ip",
            "domain"
          ]
        },
        "certChallenge": {
          "type": "string",
          "enum": [
            "http-01",
            "dns-01"
          ],
          "x-flui-note": "`http-01` is applied everywhere. `dns-01` is per-runtime: a single-node vOps deploy does not implement it — the value is accepted and carried into the plan, and when the ingress is attached it warns by name and issues the certificate over http-01 instead, so no wildcard is obtained."
        },
        "certificateProvider": {
          "type": "string",
          "enum": [
            "lets-encrypt",
            "lets-encrypt-staging"
          ]
        },
        "httpsRequirement": {
          "type": "string",
          "enum": [
            "required",
            "recommended",
            "none"
          ],
          "description": "What the APPLICATION needs, as opposed to `tls`, which is what the operator requests. Some applications cannot work over plain HTTP at all — an unconditional HTTP→HTTPS redirect, `Secure` cookies, HSTS, a service worker, WebAuthn, Web Crypto, an OAuth callback registered as https. `required`: a deploy that would not be reachable over HTTPS (TLS off, or no domain at all) is refused, naming this field. `recommended`: the same deploy is warned about by name and proceeds. `none`, and an absent field, mean silence."
        }
      }
    },
    "linkedBuildingBlock": {
      "type": "object",
      "required": [
        "ref",
        "envMapping"
      ],
      "additionalProperties": false,
      "description": "Declares this standalone app is a client/UI for a building block (e.g. pgweb → postgresql). At runtime, the user connects the client to a running BB install via POST /catalog/installs/:id/connect with body { targetInstallId }. The backend wires the env vars below with `secretKeyRef` to the BB's existing K8s Secret (passwords never leave the cluster) and literal values for non-secret sources (host, port, db name). Connection is mutable: the same client can be disconnected or switched to another BB without reinstalling.",
      "properties": {
        "ref": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]{0,63}$",
          "description": "Catalog slug of the target building block."
        },
        "envMapping": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/linkedEnv"
          }
        }
      }
    },
    "linkedEnv": {
      "type": "object",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
        },
        "fromService": {
          "enum": [
            "host",
            "port"
          ],
          "description": "Compute from the BB's Kubernetes Service: 'host' → cluster DNS, 'port' → first port's internal number."
        },
        "fromBBEnv": {
          "type": "string",
          "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
          "description": "Name of an env var declared on the BB. If it is a secret (generate/userInput sensitive) the client receives it via secretKeyRef to the BB's K8s Secret; if plain, the literal value is copied."
        },
        "value": {
          "type": "string",
          "description": "Literal value emitted as a plain env entry. Used when the env is BB-specific but not derivable from the BB itself (e.g. DbGate ENGINE_xxx=mariadb@dbgate-plugin-mysql)."
        }
      },
      "oneOf": [
        {
          "required": [
            "fromService"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "fromBBEnv"
                ]
              },
              {
                "required": [
                  "value"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "fromBBEnv"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "fromService"
                ]
              },
              {
                "required": [
                  "value"
                ]
              }
            ]
          }
        },
        {
          "required": [
            "value"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "fromService"
                ]
              },
              {
                "required": [
                  "fromBBEnv"
                ]
              }
            ]
          }
        }
      ]
    },
    "dependency": {
      "type": "object",
      "required": [
        "ref",
        "as"
      ],
      "additionalProperties": false,
      "properties": {
        "ref": {
          "type": "string"
        },
        "as": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_-]{0,63}$"
        },
        "required": {
          "type": "boolean"
        },
        "reuseExisting": {
          "type": "boolean"
        }
      }
    },
    "access": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": {
          "enum": [
            "credentials",
            "firstVisit",
            "none"
          ]
        },
        "path": {
          "type": "string"
        },
        "username": {
          "$ref": "#/definitions/accessValue"
        },
        "password": {
          "$ref": "#/definitions/accessValue"
        },
        "note": {
          "type": "string"
        }
      }
    },
    "accessValue": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "fromEnv": {
          "type": "string"
        },
        "component": {
          "type": "string"
        },
        "value": {
          "type": "string"
        }
      }
    },
    "auth": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": {
          "enum": [
            "oidc",
            "proxy",
            "native",
            "none"
          ]
        },
        "modes": {
          "type": "array",
          "items": {
            "enum": [
              "oidc",
              "proxy",
              "native",
              "none"
            ]
          }
        },
        "default": {
          "enum": [
            "oidc",
            "proxy",
            "native",
            "none"
          ]
        },
        "oidc": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "redirectPath": {
              "type": "string"
            },
            "redirectPaths": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "configFile": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "path",
                "env",
                "template"
              ],
              "properties": {
                "path": {
                  "type": "string"
                },
                "env": {
                  "type": "string"
                },
                "template": {
                  "type": "string"
                }
              }
            },
            "scopes": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "envMapping": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "issuerUrl": {
                  "type": "string"
                },
                "clientId": {
                  "type": "string"
                },
                "clientSecret": {
                  "type": "string"
                },
                "enabledFlag": {
                  "type": "string"
                }
              }
            }
          }
        },
        "proxy": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "headerMapping": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "postInstall": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "when": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "authMode": {
                "oneOf": [
                  {
                    "enum": [
                      "oidc",
                      "proxy",
                      "native",
                      "none"
                    ]
                  },
                  {
                    "type": "array",
                    "items": {
                      "enum": [
                        "oidc",
                        "proxy",
                        "native",
                        "none"
                      ]
                    }
                  }
                ]
              },
              "option": {
                "type": "string"
              }
            }
          },
          "http": {
            "type": "object",
            "required": [
              "method",
              "path"
            ],
            "additionalProperties": false,
            "properties": {
              "method": {
                "enum": [
                  "GET",
                  "POST",
                  "PUT",
                  "PATCH",
                  "DELETE"
                ]
              },
              "path": {
                "type": "string"
              },
              "headers": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "body": {
                "type": "string"
              },
              "expectStatus": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          },
          "exec": {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "container": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "smokeTest": {
      "type": "object",
      "required": [
        "type"
      ],
      "additionalProperties": false,
      "description": "The post-deploy gate: the one check that decides whether the deploy is kept or rolled back. Distinct from `healthcheck`, which runs inside the container forever — this runs once, from the host, after the units are up. This definition is shared field for field with the other manifest kind, kept deep-equal by a parity test. A client that cannot run the kind declared here must say so; substituting a weaker check and reporting success is a defect, not a fallback.",
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "http",
            "tcp",
            "script",
            "skip"
          ],
          "description": "http requests `path` and compares the status; tcp connects to the published port; script runs `inline` (or `file`/`shell`); skip declares that no gate is possible and says why."
        },
        "reason": {
          "type": "string",
          "description": "Why there is no gate. Only meaningful with type: skip, where it is what a reader gets instead of a check."
        },
        "path": {
          "type": "string",
          "description": "Path requested by an http gate. Defaults to `/`."
        },
        "expectedStatus": {
          "type": "integer",
          "minimum": 100,
          "maximum": 599,
          "description": "Status an http gate treats as success. Declare it for an app whose root answers 302 or 401 when healthy, or the deploy rolls back an application that is serving correctly."
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "description": "Container port a tcp gate connects to. Defaults to the primary published port."
        },
        "inline": {
          "type": "string",
          "description": "Shell commands run as the gate, for a service that answers on a protocol rather than on HTTP (a database round-trip). Requires type: script."
        },
        "file": {
          "type": "string",
          "description": "Path to a script run as the gate, instead of `inline`."
        },
        "shell": {
          "type": "string",
          "description": "Interpreter for `inline`/`file`. Defaults to the image's `sh`."
        },
        "timeoutSeconds": {
          "type": "integer",
          "minimum": 1,
          "description": "How long the application has to answer for the FIRST time. This is a startup budget, not a request timeout: a cold host pulling an image and running migrations is nothing like the warm local runtime a manifest was written against, and too small a value rolls back an application that is merely slow.",
          "x-flui-note": "The spec sets no maximum. Per-runtime: a single-node vOps deploy clamps the effective window to 120s at the low end and 600s at the high end — a smaller value never shortens the budget below the default, and a larger one is accepted and capped, so a single bad number cannot hang a deploy for an hour."
        },
        "retries": {
          "type": "integer",
          "minimum": 0,
          "description": "Probe attempts. Attempts times the client's fixed spacing is a second way of expressing the same startup budget; the larger of the two wins."
        }
      }
    }
  }
}
