Skip to main content

Publish a template

A template is a real Kuploy stack — a NativeStackSpec — plus a catalog entry describing it. Someone browsing kuploy.app/templates sees the entry; what they import is the spec. It is not a Compose catalogue: the unit is a Kuploy stack, and paid templates reuse kuploy-hub's existing billing rather than a parallel paywall.

Templates live in kuploy/kuploy-templates.

Repository layout​

meta.json                     # the catalog
templates/<id>/stack.yaml # the stack spec that gets imported
templates/<id>/logo.svg
scripts/validate-meta.mjs # CI check

The catalog entry​

Each entry in meta.json's templates array:

FieldRequiredWhat it is
idyesUnique slug; also the directory name under templates/
nameyesDisplay name in the gallery
versionyesTemplate version, not the upstream app's
descriptionyesOne paragraph; it is the gallery's copy
logoyesPath to the SVG, e.g. templates/<id>/logo.svg
stackyesPath to the spec, e.g. templates/<id>/stack.yaml
linksyesgithub, website, docs — whichever apply
tagsyesSearch and filtering
accessyesfree, pro or cloud
productonly when access isn't freeThe kuploy-hub product.slug, plus an optional plan slug
variablesnoSecrets a provisioning flow should generate — {key, generate, component}
domainsnoComponents that need a hostname
{
"id": "learnhouse",
"name": "LearnHouse LMS",
"version": "0.1.0",
"description": "Open-source LMS: courses, quizzes, assignments, certificates.",
"logo": "templates/learnhouse/logo.svg",
"stack": "templates/learnhouse/stack.yaml",
"links": {"github": "https://github.com/learnhouse/learnhouse"},
"tags": ["lms", "education"],
"access": "free",
"product": null,
"variables": [{"key": "NEXTAUTH_SECRET", "generate": "random32", "component": "learnhouse-app"}],
"domains": ["learnhouse-app"]
}

Access tiers​

access decides who may see the spec, and the check runs against billing you already have:

accessWho gets the spec
freeEveryone, signed in or not
pro / cloudA tenant with an active or trialing subscription to the named product

A viewer without that subscription sees the entry and a View plans link rather than the spec; an anonymous viewer is asked to sign in. The gate reads tenant_subscription for the product.slug you name — there is no separate entitlement store to populate.

A non-free template must name its product

validate-meta.mjs fails the build when access is pro or cloud and product.slug is missing. Without it the gallery has nothing to check, so the template would be either free to all or unusable.

Validate before you open the PR​

node scripts/validate-meta.mjs

It checks the required fields, that ids are unique, that the stack and logo paths actually exist, that access is one of the three values, and the product rule above. CI runs the same script, so a template that passes locally passes review.

What a user does with it​

From the gallery, Import into kuploy copies the stack spec to the clipboard; they paste it into Stacks → Import in their environment. Deploy on cloud hands the template to a Kuploy Cloud instance instead.

The import dialog offers two modes, and your entry feeds the first:

  • Provision new services creates each component from the spec, generates the secrets you declared under variables (random32, keypair) and assigns a hostname to each component in domains. A value can reference the assigned host with the ${domain} token.
  • Wire existing services matches components to services the user already has, by name, and only creates the env connections between them.

So declare variables for every secret the app needs rather than hard-coding a placeholder: on the provisioning path they are generated per deploy, and a hard-coded one would be shared by everyone who imports your template.

Writing the stack spec​

The spec is an ordinary Kuploy stack, so the developer guide for Stacks is the reference. Two things are worth doing for a template specifically:

  • Pin image tags. latest makes a template that worked last month fail today, and the person importing it has no idea why.
  • Document deploy caveats in the description. Anything the stack can't express — LiveKit's UDP and TURN requirements, for instance — belongs in the text, because that is all the user reads before importing.