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:
| Field | Required | What it is |
|---|---|---|
id | yes | Unique slug; also the directory name under templates/ |
name | yes | Display name in the gallery |
version | yes | Template version, not the upstream app's |
description | yes | One paragraph; it is the gallery's copy |
logo | yes | Path to the SVG, e.g. templates/<id>/logo.svg |
stack | yes | Path to the spec, e.g. templates/<id>/stack.yaml |
links | yes | github, website, docs — whichever apply |
tags | yes | Search and filtering |
access | yes | free, pro or cloud |
product | only when access isn't free | The kuploy-hub product.slug, plus an optional plan slug |
variables | no | Secrets a provisioning flow should generate — {key, generate, component} |
domains | no | Components 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:
access | Who gets the spec |
|---|---|
free | Everyone, signed in or not |
pro / cloud | A 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.
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 indomains. 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.
latestmakes 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.