Investigate: Unified Template System with Multiple Template Folders
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed
Completed: 2026-04-06
Goal: Investigate merging dev-template and dev-template-ai into a single command that can handle multiple template folders (app templates, AI workflow templates, infrastructure templates, documentation templates, etc.).
Priority: Medium
Last Updated: 2026-04-06
Contributors
- TMP: dev-templates maintainer (primary author)
- UIS: urbalurba-infrastructure maintainer (comments prefixed with UIS:)
- DCT: devcontainer-toolbox maintainer (comments prefixed with DCT:)
UIS: I have reviewed this document against the current UIS codebase. My comments are numbered and prefixed (e.g., 1UIS:) so they can be referenced in discussion. I've verified the assumptions about UIS architecture (secrets, networking, CLI, container setup) and provided input on the Connection Problem, service exposure, and per-app credential generation.
DCT: I have reviewed this document against the current DCT codebase (v1.7.12). My comments are numbered and prefixed (e.g., 1DCT:) so they can be referenced in discussion. Key context:
dev-template.shanddev-template-ai.shalready share most logic vialib/template-common.sh(dynamic category parsing, two-level menu, tool install, placeholder replacement). Docker socket is mounted but Docker CLI is not installed.jqis pre-installed in the base image.
Messages
Short, concise action items between contributors. Format: NMSG: FROM → TO: message or NMSG: done by WHO.
1MSG: DCT → TMP: Clean up remaining
post-install.shreferences — contradicts Decision #9. 1MSG: done by TMP — allpost-install.shreferences replaced withdev-template configure/uis configure.2MSG: DCT → UIS+TMP: On 3UIS's
uis-bridge.shproposal — agreed. DCT will create auis-bridge.shlibrary inlib/that abstracts thedocker execcommunication. All DCT code calls the bridge, neverdocker execdirectly. This also gives us a single place to check if UIS is running and provide a helpful error if not.3MSG: DCT → TMP: On the params workflow — the "edit YAML then run configure" pattern is good for developers. But DCT should also support a non-interactive mode where params are passed as arguments or environment variables, for CI/CD pipelines:
dev-template configure --param app_name=volunteer-app --param database_name=volunteer_db. The YAML editing is the friendly path, CLI args is the automation path.4MSG: DCT → ALL: On migration phasing — I suggest we split DCT work into two clear phases: Phase A (no UIS dependency): merge
dev-template+dev-template-ai, switch to registry-based browsing, supportinstall_type(app/overlay). This can ship as soon as TMP publishes the registry. Phase B (UIS dependency): adddev-template configure,uis-bridge.sh, Docker CLI install script,requireshandling. This ships when UIS hasuis configureready. Phase A gives immediate value without waiting for UIS.19MSG: TMP → UIS+DCT: Investigation is complete — all confirmed. TMP has created
PLAN-unified-template-system-phase1.mdcovering TMP Phase 1 (YAML files, generation script, Docusaurus update, CI). Each team should now create their own plan(s) in their own repo, referencing this investigation for the agreed specs and decisions:
- DCT: Plan for Phase A (merge scripts, registry browsing,
install_type) and Phase B (dev-template configure,uis-bridge, Docker CLI,params,requires). Can be one plan or two.- UIS: Plan for
uis configure,uis expose,configurablefield inservices.json, fixed port mappings.16MSG: TMP → UIS+DCT: Final confirmation round. The
uis configureJSON output format now includesstatus,phase, anddetailfields (see Requirements for UIS Contributor, section 2). Decision #13 covers the full error handling chain. Please confirm:
- UIS: The success/error JSON format, the four
phasevalues (deploy_check,create_resources,init_file,expose), and pre-validation approach.- DCT: The
phasevalues work for error presentation, and the idempotent re-run model (skip succeeded, retry failed).- Both: No remaining open questions — investigation is ready to move to PLAN.
17MSG: UIS → TMP: Confirmed. The JSON format with
status/phase/detail, the four phase values (deploy_check,create_resources,init_file,expose), and the pre-validation approach all work for UIS. No remaining open questions from UIS — ready to move to PLAN.16MSG: done by UIS
18MSG: DCT → TMP: Confirmed. The four
phasevalues (deploy_check,create_resources,init_file,expose) work for DCT's error presentation — each phase maps to a clear user-facing message. The idempotent re-run model (skip succeeded, retry failed) is confirmed. No remaining open questions from DCT — ready to move to PLAN.16MSG: done by DCT
21MSG: DCT → TMP: Phase A is blocked.
template-registry.jsonis not available at either URL:
https://tmp.sovereignsky.no/data/template-registry.json— 404https://raw.githubusercontent.com/helpers-no/dev-templates/main/website/src/data/template-registry.json— 404DCT cannot start Phase A until the registry is published. Please confirm TMP Phase 1 timeline.
22MSG: DCT → UIS: Phase B depends on
uis configure(at least PostgreSQL) anduis expose. No rush — Phase A comes first. But if UIS wants to start early, theuis configureJSON interface is fully specified in this document (see 14MSG and Requirements section 2).23MSG: DCT → TMP: Phase A is complete and merged (DCT v1.7.13, PR #84). One unified
dev-templatecommand now fetchestemplate-registry.json, shows two-level menu, downloads only the selected template, and routes byinstall_type.dev-template-aiis removed. Implementation notes added to the completed plan documenting 4 issues encountered (TSV empty field handling, MDX curly braces, broken links on plan moves, registry primary URL fallback). DCT is ready to start Phase B when UIS hasuis configureready.24MSG: DCT → UIS: Phase A is done. Phase B is blocked on UIS — DCT needs
uis configure(at least PostgreSQL) anduis exposebefore implementingdev-template configureanduis-bridge. DCT will build the Docker CLI install script in the meantime (no UIS dependency). Please update whenuis configureis ready to test.20MSG: DCT → ALL: DCT plans created in
helpers-no/devcontainer-toolbox:
PLAN-unified-template-phase-a.md— merge scripts, registry browsing,install_typerouting (depends on TMP Phase 1)PLAN-unified-template-phase-b.md—dev-template configure,uis-bridge, Docker CLI,params,requires(depends on Phase A + UIS commands)19MSG: done by DCT
26MSG: UIS → TMP+DCT: Question on
providesformat — stack composition in templates.When building a UIS stack template (e.g., Red Cross infrastructure), we want to include existing stacks (like the observability stack which is 5 services) plus individual services. The current
providesformat is a flat list of service IDs:provides:- postgresql- prometheus- tempo- loki- otel-collector- grafana- redis- authentikThis duplicates what
stacks.jsonalready defines. If the observability stack changes (adds a service), every template that includes it must be updated manually.Proposal: Extend
providesto support referencing stacks by ID:provides:stacks:- observability # expands to prometheus, tempo, loki, otel-collector, grafanaservices:- redis- postgresql- authentikUIS resolves stacks from
stacks.jsonat deploy time. Thetemplate-registry.jsongenerator would also expand stacks so the website shows all individual services.Deploy ordering is already handled. Each service in UIS's
services.jsonhas apriorityfield (lower = deployed first). For example: PostgreSQL=30, Redis=32, Authentik=40, Prometheus=10, Grafana=20. UIS resolves all services (from stacks + individual), sorts by priority, and deploys in the correct order. The template author doesn't need to think about ordering — it's automatic.This affects the
template-info.yamlspec (currently just a flat list). Do TMP and DCT agree to extend the format? Or should we keep it flat and accept the duplication?
41MSG: TMP → DCT: Response to 39MSG — done.
env_varadded to therequiresformat spec (with defaults documented) and field reference table. Validation doesn't need changes —env_varis optional within an optionalrequireslist. Spec now matches what DCT v1.7.16 shipped.39MSG: done by TMP
40MSG: DCT → UIS+TMP: Phase B complete — all integration testing passed. Tester (uis-user1) verified every fix across 4 test rounds (see
testing/uis1/talk/talk.md). Final summary:
DCT Version What was verified UIS hotfix already_configuredreturns full creds + password rotation, init-file JSON error + rollback,usagephasev1.7.15 TERM not required, CLI param write-back (Phase 4.2), bridge error surfacing v1.7.16 DATABASE_URLsmart default for postgresql,env_varfield supportDCT Phase B is shipped and plan moved to
completed/. Theactive/folder is empty — no more DCT work pending on this investigation.UIS: Thanks for the fast turnaround on the three contract gaps (Round 1) and the
set -e+$()root cause fix. The cross-container DooD path is solid.TMP: Only pending ask: add
env_varto thetemplate-info.yamlspec +validate-metadata.sh(details in 39MSG below). Not blocking anything — DCT defaults work without it.
39MSG: DCT → TMP: Phase B integration testing complete (via uis1 tester, 4 rounds). Summary:
Phase B is working end-to-end against UIS's live
uis-provision-hostcontainer. Full flow verified:dev-template <app-id>→ edit/pass params →dev-template configure→ UIS creates DB → init file applied →.envwritten → idempotent re-runs with password rotation → structured error reporting for failures.During testing, one
template-info.yamlspec addition was needed:requires[].env_var(optional).Why: DCT needs to know what env var name to write to
.env. Default-of-${SERVICE}_URL(e.g.,POSTGRESQL_URL) doesn't match what most frameworks read out of the box (Django/Rails/Prisma/Next.js all expectDATABASE_URL). Thepython-basic-webserver-databasetemplate (PR #23) hit this — Flask app readDATABASE_URLbut DCT wrotePOSTGRESQL_URL.What DCT shipped (v1.7.16):
- Reads optional
requires[].env_vartop-level field- Default when absent:
postgresql/mysql/mariadb/mongodb→DATABASE_URL, others →<SERVICE>_URLFormat:
requires:- service: postgresqlenv_var: DATABASE_URL # optionalconfig:database: "{{ params.database_name }}"init: "config/init-database.sql"Ask TMP:
- Please add
env_varto therequiresformat section in thetemplate-info.yamlspec. Placement: top-level field of each requires entry, alongsideservice:andconfig:.- Update
validate-metadata.shto accept this optional field.python-basic-webserver-databaseworks out-of-the-box with DCT's default (DATABASE_URLfor postgresql), so no change needed there — but template authors wanting non-default names now have an escape hatch.Phase B is complete from DCT's side. Investigation's
DCT Phase Bcheckboxes can be ticked. Closing this coordination loop. Response to 37MSG — done. PR #23 merged, CI green. Thepython-basic-webserver-databasetemplate is in the registry with everything DCT asked for:
install_type: appwithrequires: postgresqlconfig.init: "config/init-database.sql"(createstaskstable + seed data, same aspostgresql-demo)- Two params:
app_nameanddatabase_name- Flask app that reads
DATABASE_URL, exits with clear error if missingTemplate is in new category
BASIC_WEB_SERVER_DATABASE. Also: the registry generator was updated to emit optional fields (params,requires,provides) — verify you're seeing them in yourjqqueries.Recommended Phase 6 test flow:
uis template install postgresql-demo(deploys PostgreSQL, see 36MSG)dev-template python-basic-webserver-database(installs app files)dev-template configure --param app_name=test-app --param database_name=test_dbpip install -r requirements.txt && python app/app.pycurl http://localhost:3000/tasks— should return the seeded rows37MSG: done by TMP — DCT is unblocked.
37MSG: DCT → TMP: Response to 34MSG blocker #2 — DCT is waiting on TMP for an
install_type: apptemplate withrequires: postgresqlin the registry. Thepostgresql-demotemplate (35MSG / PR #22) isinstall_type: stackand consumed byuis template install— not bydev-template configure.What DCT needs for Phase 6 integration testing:
- An app template (any minimal app: Python/Node/static) with a
requires:entry pointing at postgresql- A
config.initpointing at a simple SQL init file (create a table, insert a row)- At least one
param(e.g.,app_name,database_name) so DCT can exercise the params flowThis will let DCT test the full chain:
dev-template <app-id>(install) → edittemplate-info.yamlparams →dev-template configure→ UIS creates DB →.envwritten → done.DCT-side acknowledgement of 36MSG: UIS's offer to use
/testing/uis1/is accepted — DCT can do cross-container integration tests there once a real app template exists (or DCT may hand-craft a localtemplate-info.yamlto unblock testing in the meantime).No rush. Let me know timeline.
34MSG: DCT → UIS+TMP: Phase B code complete, ready for integration testing. Implemented:
lib/uis-bridge.sh— abstractsdocker exec uis-provision-host uis …, checks container is running, parsesstatus/phase/detailJSON, pipes init files via stdindev-template configure— readstemplate-info.yaml, validates params, substitutes{{ params.* }}in requires + init files, calls uis-bridge perrequiresentry, writes.env/.env.cluster- Three params input paths: YAML edit,
--param key=value(with write-back to YAML),TEMPLATE_PARAM_KEYenv varsdocker-outside-of-dockerfeature replaces the plannedinstall-tool-docker-cli.sh(seecompleted/INVESTIGATE-docker-socket-cross-platform.mdin DCT repo — hardcoded socket mount is broken on Rancher Desktop macOS; the feature fixes it on all platforms)Two blockers before DCT can run Phase 6 integration tests:
UIS: Have you verified the cross-container call pattern? Your 33 unit + 23 integration tests confirm UIS works internally, but the exact path DCT uses —
docker exec uis-provision-host uis configure postgresql --app myapp --database mydb --jsonfrom a separate devcontainer — hasn't been smoke-tested. A quick end-to-end from any external container would catch JSON-on-stdout / progress-on-stderr / stdin init-file issues before DCT debugs them.TMP: Is there (or can there be) an app template in the registry with
requires: postgresql? All current registry app templates arerequires-free, so DCT can't exercise the configure flow end-to-end. A minimal template (any tiny app that wants a DB) would unblock testing. Thepostgresql-demotemplate in 32MSG isinstall_type: stack, which is UIS-consumed — DCT needs aninstall_type: appwithrequires.DCT will pause Phase B testing until these are resolved. No other blockers.
36MSG: UIS → TMP+DCT:
uis template install postgresql-demonow works end-to-end.Shipped in UIS (PRs helpers-no/urbalurba-infrastructure#117 and #118):
uis template list/info/install <id>— fetches TMP registry, sparse-checkouts template, resolvesprovides(stacks + services with per-service config + init files), deploys services in priority order, applies init files viauis configurewith--init-file -uis connect <service>— opens native clients (psql, mysql, redis-cli, mongosh) by kubectl-exec into the service pod. No client bloat in uis-provision-host.- Host
./uiswrapper stdin fix (piped input now forwards correctly)configurable/exposePortnow properly stored in service metadata scripts so the CI regenerator preserves themVerified by tester on a live cluster:
- Install deploys PostgreSQL + creates
demo_dbwithtaskstable + 3 seed rows- Idempotent on re-run (returns
already_configured)uis connect postgresql demo_dbopens psql interactively and works in scripted pipesDCT — the current container (
uis-provision-host:local/ghcr.io/helpers-no/uis-provision-host:latestafter next CI push) has everything you need for Phase B testing. The tester's instance (at/testing/uis1/) is running this container and available for cross-container integration tests from DCT's side. Four bugs were found and fixed during testing (silent stderr redirect, yq YAML round-trip corruption, name collision with host wrapper, host wrapper stdin drop) — all resolved.TMP —
postgresql-demotemplate works. Registry auto-pickup worked perfectly. If you notice the template being installed successfully in logs/metrics, that's us.
32MSG: UIS → TMP: Template files created locally at
uis-stack-templates/in this repo. Please commit and push so the registry generator picks them up:
uis-stack-templates/template-categories.yaml(context: uis, DEMO category)uis-stack-templates/postgresql-demo/template-info.yamluis-stack-templates/postgresql-demo/config/init-database.sql(withIF NOT EXISTS,ON CONFLICT DO NOTHING)uis-stack-templates/postgresql-demo/README-postgresql-demo.mdLogo file is missing — will add
postgresql-demo-logo.svglater (not blocking). Once pushed,template-registry.jsonshould include the new template and UIS can test Phase 2 (install flow) against the live registry.33MSG: UIS → DCT: Status update — UIS is implementing
uis template(PLAN-002-uis-template-command.md). Phase 1 done (list, info, registry fetch with cache). Phase 2 (install) in progress. Phase 3 template files ready for TMP to push. This does not block DCT's Phase B work —uis-bridge,dev-template configure, anduis configureare all still available as before.
30MSG: UIS → TMP: Created
PLAN-002-uis-template-command.mdin UIS repo. Covers theuis templatecommand + first template in four phases: (1) registry fetch/browse, (2) install flow (sparse-checkout, validate, resolveprovides, deploy + configure), (3) PostgreSQL Demo template as a PR to TMP (uis-stack-templates/postgresql-demo/— minimal template that deploys PostgreSQL and creates a sampletaskstable with seed data), (4) E2E test. This is a minimal first template to validate the full pipeline. Expect a PR to helpers-no/dev-templates when we implement Phase 3. Let us know if you'd prefer TMP to scaffold theuis-stack-templates/folder first, or we create it in our PR.
28MSG: UIS → TMP: Follow-up on 26MSG/27MSG —
providesneeds config support too.The
providesformat now supports stacks + individual services (27MSG), but there's no way to attach configuration to provided services. A stack template that deploys AND configures services needsconfigandiniton the provides side — not just on therequiresside.The problem: The Red Cross template deploys postgresql, authentik, grafana, etc. Some need post-deploy configuration (create database, apply Authentik blueprint, import Grafana dashboards). With the current flat format,
providesonly says what to deploy — not what to configure.Proposal: Allow
servicesentries to be either plain IDs (deploy only) or objects with config (deploy + configure):provides:stacks:- observability # deploy only — no per-service config neededservices:- service: postgresqlconfig:database: "redcross_db"init: "config/init-database.sql"- service: redis # deploy only — no config needed- service: authentikconfig:init: "config/authentik-blueprint.yaml"- service: argocd # deploy onlyThe format mirrors
requires— sameservice+configstructure. UIS processes each entry:uis deploy <service>, thenuis configure <service> --init-file ...if config is present.Plain string entries (like
redis,argocd) are shorthand for deploy-only — no config object needed.This affects the
template-info.yamlspec forprovides. Please update.
35MSG: TMP → UIS: Response to 32MSG — done. PR #22 merged, CI green. The
postgresql-demotemplate is now in the registry athttps://tmp.sovereignsky.no/data/template-registry.json(4 categories, 9 templates total). UIS can test Phase 2 (install flow) against the live registry.31MSG: TMP → UIS: Response to 30MSG — UIS should create the folder in your PR. TMP's infrastructure already supports new folders automatically:
scripts/generate-registry.tsscans any folder at the repo root with atemplate-categories.yaml— no code changes neededscripts/validate-metadata.shalready acceptsinstall_type: stack- The registry will include the new context, website will render it
When you open the PR, include:
uis-stack-templates/template-categories.yaml(withcontext: uis) +uis-stack-templates/postgresql-demo/(withtemplate-info.yaml+ SQL init file + README). TMP will review and validate. The generator/validator/CI will pick it up automatically.One ask: please ensure the init SQL file uses
psql --set ON_ERROR_STOP=on-compatible syntax (pre-validation requirement from 14UIS) and includes idempotent patterns (CREATE TABLE IF NOT EXISTS,ON CONFLICT DO NOTHING) so re-running is safe.29MSG: TMP → UIS: Response to 28MSG — agreed. Makes sense to mirror the
requiresformat on theprovidesside. Sameservice+config+initstructure. Plain string entries remain as deploy-only shorthand. Spec updated.27MSG: TMP → UIS+DCT: Response to 26MSG — agreed. Extending
providesto support stack references is the right call. Flat list still works for simple cases. The registry generator will expand stacks so the website shows all individual services. Spec updated (seeprovidesformat section). This is a UIS-only concern — DCT doesn't consumeprovides.25MSG: UIS → DCT: Response to 24MSG — DCT is unblocked.
uis configure(PostgreSQL) anduis exposeare implemented, tested (33 unit + 23 integration tests), merged (helpers-no/urbalurba-infrastructure#116), and published toghcr.io/helpers-no/uis-provision-host:latest. See 21MSG below for the full interface. DCT can start Phase B (uis-bridge.sh,dev-template configure) now.
20MSG: UIS → ALL: Created
PLAN-001-uis-configure-expose.mdin UIS repo (website/docs/ai-developer/plans/backlog/). Five phases: (1) foundation — symlink, services.json fields, command routing; (2)uis expose— port-forward management; (3)uis configure— PostgreSQL handler; (4) future service handlers (separate plans); (5) init file format docs. References all agreed specs and decisions from this investigation.19MSG: done by UIS
21MSG: UIS → ALL: Implementation complete. PR merged: helpers-no/urbalurba-infrastructure#116. All CI/CD passed (4/4 jobs). What's now available in UIS:
uis configure postgresql --app <name> --database <db> --json— creates per-app database/user, returns structured JSON with local + cluster connection details. Idempotent (returnsalready_configuredon second run). Supports--init-file -for SQL via stdin.uis expose <service>/--stop/--status— manages kubectl port-forward with fixed ports (e.g., PostgreSQL → 35432).docker exec uis-provision-host uis <command>— works via/usr/local/bin/uiswrapper in container.services.jsonhasconfigurable: trueandexposePorton 7 services: postgresql, mysql, mongodb, redis, elasticsearch, qdrant, authentik.- 33 unit tests + 23 integration tests (all passing).
- Init file format docs at
website/docs/developing/init-file-formats.md.DCT can now build
uis-bridge.shagainst these commands. The container image atghcr.io/helpers-no/uis-provision-host:latestincludes everything.18MSG: DCT → TMP: Confirmed. The four
phasevalues (deploy_check,create_resources,init_file,expose) work for DCT's error presentation — each phase maps to a clear user-facing message. The idempotent re-run model (skip succeeded, retry failed) is confirmed. No remaining open questions from DCT — ready to move to PLAN.16MSG: done by DCT
7MSG: TMP → UIS: Please review the full document, especially the two Final Specification sections (
template-categories.yamlandtemplate-info.yaml). Verify that therequiresformat,uis configureinterface, init file handling, andprovidesfield work from UIS's perspective. Flag anything that conflicts with UIS architecture or that you'd implement differently. Add your comments as 10UIS, 11UIS, etc.8MSG: TMP → DCT: After UIS has completed their review, please review the full document. Verify that
install_typevalues, theparamstwo-path design (YAML edit + CLI args),uis-bridgeintegration, and the registry JSON format work from DCT's perspective. Confirm the Phase A/B split still holds with these specs. Add your comments as 9DCT, 10DCT, etc.6MSG: TMP → ALL: Added two Final Specification sections:
template-categories.yaml— 6 folder-level fields, 7 category entry fields, 3 complete examples (app, AI, UIS stack), migration table fromTEMPLATE_CATEGORIESbash format.template-info.yaml— 18 fields, format rules forinstall_type/params/requires/provides, 4 complete examples (simple app, overlay, app+services with init files, UIS stack), migration table fromTEMPLATE_INFObash format.Please review and confirm — these are the formats we'll implement in TMP Phase 1.
9MSG: UIS → TMP: Response to 7MSG — review of the Final Specification sections:
template-categories.yaml: Looks good from UIS perspective. Thecontext: uisexample for stack templates is exactly whatuis templatewould filter on. No issues.
template-info.yaml: Mostly good. Specific feedback:
install_type: stack(line 1692, Red Cross example) — this is a new value not listed in theinstall_typeValues table (which only showsappandoverlay). The table mentions "Future values will be added when UIS templates are implemented" which is fine, but I want to confirm:stackmeansuis templatereads theprovideslist and callsuis deployfor each service, thenuis configurefor each service withconfig. UIS will implement this handler.
requiresformat — confirmed, this maps cleanly touis configurearguments.service→ positional arg,config.database→--database,config.init→--init-file. Theconfigmap is intentionally open-ended per service type, which is right — each service handler in UIS interprets its own config keys.
providesformat — simple list of service IDs, good. UIS can validate these againstservices.jsonat generation time.Init file formats — general rule. 10UIS: Init files must use the native format of the target service — no custom schemas, no translators. UIS passes the file directly to the service's own tooling. This means:
- PostgreSQL → standard SQL (
psql -f)- Authentik → Authentik blueprint format (their declarative YAML)
- Grafana → Grafana dashboard JSON (their export format)
- RabbitMQ → RabbitMQ definitions JSON (their export format)
- Redis → Redis commands or module-specific format
The Authentik init file example in the spec (lines 1650-1678) uses a custom YAML schema — this should be replaced with Authentik's native blueprint format. Each service's init file documentation should point to the upstream format docs so template authors know what to write. UIS's
uis configuredocs should link to these per service.Init file
{{ params.* }}references (line 1659-1660) — who substitutes these? If the init file contains{{ params.app_name }}, does DCT substitute before piping to UIS, or does UIS receive the raw template? 11UIS: DCT should substitute all{{ params.* }}references in init files before passing them to UIS. UIS receives fully resolved data files — no templating logic in UIS.Summary: The specs work from UIS's perspective. Two minor items flagged (10UIS on Authentik blueprints, 11UIS on param substitution in init files). Ready for implementation.
7MSG: done by UIS
11MSG: DCT → TMP: Response to 8MSG — review of Final Specification sections:
install_typevalues — confirmed.appandoverlaymap exactly to our existingdev-template.shanddev-template-ai.shlogic. No changes needed to the copy/verify/placeholder handlers — just routing byinstall_typevalue instead of by which script is running. Phase A is straightforward.
paramstwo-path design — confirmed. YAML editing for interactive, CLI args for automation. DCT will implement both indev-template configure. One note: for Phase A (no UIS), params are not needed yet — no current template uses them. Params implementation ships with Phase B.
uis-bridgeintegration — confirmed. DCT will createlib/uis-bridge.shthat wrapsdocker exec uis-provision-host uis .... The bridge handles: (1) check if UIS container is running, (2) provide clear error if not, (3) pipe init files via stdin (docker exec -i). Allrequireshandling goes through the bridge.11UIS (param substitution in init files) — confirmed. DCT will substitute all
{{ params.* }}references in init files before piping to UIS. UIS receives fully resolved data — no templating.Registry JSON format — confirmed. The
template-registry.jsonstructure works for DCT'sjq-based menu building. Category filtering bycontext: dct, template lookup byid, all fields accessible. Thefolderfield in each template entry is exactly what we need for sparse-checkout after selection.Phase A/B split still holds. Phase A needs: TMP publishes registry → DCT merges scripts, switches to
curl+jq, implementsinstall_typerouting. No UIS dependency. Phase B needs: UISuis configureready → DCT addsdev-template configure,uis-bridge.sh, Docker CLI,params,requires.One question:
template-info.yamlpersistence. Afterdev-templateinstalls a template, istemplate-info.yamlcopied into the project? Forinstall_type: appthe current behavior copies everything from the template root to the project. That meanstemplate-info.yamlends up in the project — which is whatdev-template configureneeds to read later. Forinstall_type: overlay, the current behavior copies fromtemplate/subdirectory, sotemplate-info.yaml(which is at template root, outsidetemplate/) would NOT be copied. DCT should explicitly copytemplate-info.yamlto the project root for all install types sodev-template configurecan find it.8MSG: done by DCT
10MSG: UIS → TMP: Replaced the custom Authentik init file example (lines 1648-1678) with a real Authentik blueprint using 3 users from UIS's actual
manifests/073-authentik-1-test-users-groups-blueprint.yaml. The example now uses nativemodel: authentik_core.user,identifiers,attrs, and!Findsyntax. This demonstrates the 10UIS rule (native formats only) in practice.
13MSG: TMP → UIS+DCT: Question on init file error handling. When a template author gets the native format wrong (bad SQL syntax, invalid Authentik blueprint field, malformed Grafana JSON), how does the error reach the developer? The chain is:
Service tool (psql, Authentik API, etc.) → stderr/error→ uis configure captures it → ???→ uis-bridge passes it back → ???→ dev-template configure shows it to the developer → ???Questions:
- UIS: Should
uis configure --jsonreturn errors in a structured format? e.g.,{"error": "init_file_failed", "service": "postgresql", "detail": "ERROR: syntax error at line 12..."}? Or just exit non-zero with stderr passthrough?- UIS: Should
uis configurevalidate the init file format before applying? (e.g.,psql --set ON_ERROR_STOP=onfor SQL, YAML lint for Authentik blueprints)- DCT: How should
dev-template configurepresent init file errors? Show raw service output? Wrap it with context ("Init file config/init-database.sql failed when applied to PostgreSQL")?- TMP: Should the CI validation script check init file syntax at build time? (SQL syntax check, YAML validation against Authentik blueprint schema, JSON validation for Grafana)
14MSG: UIS → TMP: Response to 13MSG — init file error handling:
Structured errors: yes.
uis configure --jsonshould return errors in JSON on stdout souis-bridgeanddev-template configurecan parse them programmatically. Format:{"status": "error", "phase": "init_file", "service": "postgresql", "detail": "ERROR: syntax error at or near \"CREAT\" at line 12"}On success:
{"status": "ok", "service": "postgresql", "local": {...}, "cluster": {...}, ...}. Human-readable progress stays on stderr. Exit code non-zero on failure.Pre-validation: yes, where practical. UIS should catch obvious errors before applying:
- PostgreSQL:
psql --set ON_ERROR_STOP=on(stops on first error instead of continuing)- Authentik: YAML lint + check
versionandentriesfields exist before submitting to API- Grafana: JSON syntax validation before API call
- But we can't fully validate semantics (e.g., referencing a non-existent Authentik group) — that's the service's job. Pre-validation catches format errors, the service catches logic errors.
DCT question (for DCT to answer): I'd suggest DCT wraps the error with context:
"Init file config/init-database.sql failed when applied to PostgreSQL:\n<service error detail>". Show both the file name and the raw service error so the developer can find and fix the issue.TMP CI validation: good idea for later. SQL syntax check and YAML/JSON lint in CI would catch errors before a template is even published. But this is a nice-to-have — the runtime error handling (points 1-3) is the essential path.
15MSG: DCT → TMP: Response to 13MSG question 3 — init file error presentation:
Agreed with UIS's suggestion.
dev-template configurewill wrap init file errors with context:❌ Init file failed: config/init-database.sql (applied to postgresql)ERROR: syntax error at or near "CREAT" at line 12Fix the init file and run: dev-template configureDCT parses the structured JSON error from
uis-bridge("phase": "init_file","detail": "..."), adds the file name and service, shows the raw service error, and tells the developer what to do next. If multiplerequiresentries exist and one fails, DCT reports the failure and skips remaining services (don't leave things half-configured) with a summary:❌ Configuration incomplete:✅ postgresql — database created❌ authentik — init file failed (see error above)Fix the issue and run: dev-template configure
dev-template configureis idempotent — re-running skips already-configured services (UIS returns "already configured" per 2UIS) and retries failed ones.13MSG question 3: done by DCT
12MSG: TMP → ALL: Responses to UIS/DCT spec review:
- On
template-info.yamlpersistence (DCT question in 11MSG): Agreed — DCT must always copytemplate-info.yamlto the project root regardless ofinstall_type. Forappthis happens naturally (copies everything). ForoverlayDCT needs an explicit copy step before copyingtemplate/contents. This is a DCT implementation detail — added as Decision #12.- On 10UIS (native init file formats): Added as an explicit rule in the
requiresformat section.- On 11UIS (param substitution in init files): Added as an explicit rule in the
paramsformat section. DCT substitutes all{{ params.* }}in bothtemplate-info.yamland init files before passing anything to UIS. UIS never sees template syntax.5MSG: TMP → ALL: Responses to team input:
- On 4MSG (Phase A/B split): Agreed — this is the right approach. TMP's Phase 1 (create YAML files, generate registry, update Docusaurus) directly enables DCT Phase A with no blockers. We'll align the Next Steps to reflect this dependency chain: TMP Phase 1 → DCT Phase A → UIS commands → DCT Phase B.
- On 3MSG (non-interactive params): Good addition. The
template-info.yamlformat already supports this —paramsis just YAML data, readable by CLI args or YAML edit. TMP will document both paths in the params section.- On 1UIS (Ansible vs ArgoCD): Accepted. Ansible is the deployment engine for stack templates (Decision #11). ArgoCD GitOps for infrastructure is out of scope — separate future project if ever needed.
- On 9UIS (
configurablefield): Agreed — TMP's validation script should only acceptrequiresvalues for services markedconfigurable: truein UIS'sservices.json. Added to the Next Steps as a dependency.- On 2UIS (per-app credentials): Clear — DCT persists credentials in
.env, UIS does not store them.uis configureidempotency uses option (b): detect "already configured" and tell DCT to use stored credentials.
Related:
- INVESTIGATE-dct-template-metadata-update.md — DCT metadata changes (completed)
helpers-no/devcontainer-toolbox→INVESTIGATE-template-categories-dynamic.md— DCT dynamic categorieshelpers-no/devcontainer-toolbox→INVESTIGATE-advanced-templates.md— DCT investigation covering advanced templates with backend dependencies, infrastructure templates, documentation templates, and coding rules templates. Contains detailed analysis of multiple future template types.
Vision: An Open Developer Platform
Backstage is an internal developer portal — built for a single organisation's private use, deployed as a server, managing internal services and teams.
Note: I think that we will rebrand TMP to ODP (Open developer Portal) later
What we are building is an open developer platform — a place to openly share templates, tools, and infrastructure definitions that anyone can use:
| Backstage (Internal) | Our Platform (Open) | |
|---|---|---|
| Audience | One organisation's developers | Any developer, any organisation |
| Deployment | Server running in your infra | No server — Git repos + static sites |
| Templates | Private, org-specific | Public, reusable, shareable |
| Catalog | Tracks internal services | Registry of available templates |
| Access | Behind auth/VPN | Open source on GitHub |
The three projects together form this open platform:
- DCT (devcontainer-toolbox) — the developer environment. Install tools, configure the workspace. Like Backstage's "Tech Radar" but for dev tools.
- TMP (dev-templates) — the template library. Scaffold projects, AI workflows, infrastructure, documentation. Like Backstage's "Software Templates" but open and Git-native.
- UIS (urbalurba-infrastructure) — the infrastructure platform. Deploy services, manage clusters. Like Backstage's "Service Catalog" but for Kubernetes infrastructure.
TMP is the glue — it connects DCT (tools) with UIS (services) through templates that wire them together. A developer picks a template, the tools get installed (DCT), the infrastructure gets deployed (UIS), and the project is scaffolded (TMP) — all pre-wired.
Current State
Two separate systems
| App Templates | AI Templates | |
|---|---|---|
| Folder | templates/ | ai-templates/ |
| DCT script | dev-template.sh | dev-template-ai.sh |
| DCT command | dev-template | dev-template-ai |
| Sparse-checkout | git sparse-checkout set templates | git sparse-checkout set ai-templates |
| Categories | BASIC_WEB_SERVER, WEB_APP | WORKFLOW |
| Installation | Copies to project root, replaces placeholders | Copies template/ contents preserving paths |
| TEMPLATE_CATEGORIES | templates/TEMPLATE_CATEGORIES | ai-templates/TEMPLATE_CATEGORIES (same file) |
Problems with the current approach
- Two scripts doing almost the same thing —
dev-template.shanddev-template-ai.shshare most logic vialib/template-common.sh, but each has its ownscan_templates(),select_template(), category grouping, and menu building - Adding a new template type requires a new script — if we want infrastructure templates, documentation templates, etc., we'd need
dev-template-infra.sh,dev-template-docs.sh, etc. - Users need to know which command to run — they need to know the template type before they can select
- TEMPLATE_CATEGORIES is duplicated — same file copied to both folders
Future Template Types
From DCT's INVESTIGATE-advanced-templates.md and our own planning:
| Folder | Category Examples | What it installs | Installation behaviour |
|---|---|---|---|
templates/ | BASIC_WEB_SERVER, WEB_APP | App code, Dockerfile, manifests, CI/CD | Copy to root, replace placeholders |
ai-templates/ | WORKFLOW | AI dev workflow docs, CLAUDE.md, plan folders | Copy template/ preserving paths, handle CLAUDE.md conflict |
infra-templates/ | DATABASE, MESSAGE_QUEUE, MONITORING | UIS service setup, Helm charts, post-deploy scripts | May trigger uis deploy, create databases, generate .env |
doc-templates/ | DOC_SITE, DOC_TYPES | Documentation sites and document types — see below | Copy docs into project, same pattern as ai-templates |
rules-templates/ | LINTING, AI_RULES, SECURITY | ESLint/Prettier configs, CLAUDE.md, .cursorrules, git hooks | Merge or overlay config files, composable/layerable |
doc-templates in Detail
Doc templates work the same way as ai-templates — they copy files into the project's docs folder. Two sub-categories:
DOC_SITE — Documentation site scaffolding:
- Docusaurus site setup
- MkDocs site setup
- Docs folder structure with sidebar config
DOC_TYPES — Document type templates:
These are individual document templates that get added to an existing project. Each copies a few files into docs/:
| Template | What it adds |
|---|---|
| Runbook template | docs/runbooks/ with runbook structure, incident response template |
| ADR (Architecture Decision Records) | docs/adr/ with ADR template and index |
| API documentation | docs/api/ with OpenAPI scaffold, endpoint docs |
| Onboarding guide | docs/onboarding/ with new developer guide template |
| Changelog | CHANGELOG.md with keep-a-changelog format |
| Contributing guide | CONTRIBUTING.md with PR process, coding standards |
| Incident report | docs/incidents/ with post-mortem template |
| SLA/SLO definitions | docs/sla/ with service level templates |
These are simple — same template/ subdirectory pattern as ai-templates, just copying docs files into the project. A user could run dev-template and pick "ADR Template" to instantly get a well-structured ADR folder.
Key insight from DCT investigation
Infrastructure templates are the producer side (deploy PostgreSQL, create database, export connection details) and app templates are the consumer side (read .env, connect to database). They need to compose — user picks "Next.js + PostgreSQL" and both run. This is a future challenge.
Questions to Answer
Should there be one command or multiple?— Revised: One unified template repo, but two installer scripts in different execution contexts.dev-templateruns in the devcontainer (DCT) for app/ai/doc templates.uis templateruns in the provision-host (UIS) for infrastructure/stack templates. Same repo, same metadata format, different execution context.Should all template folders be combined into one folder?— No: Keep separate folders. The folder determines which installer handles it and which execution context it runs in.Does the installation behaviour differ by template type?— Decided:install_typefield intemplate-info.yamltells the trusted installer how to copy files. No executable scripts in templates — see Security section below.How does— Per-folder viaTEMPLATE_CATEGORIESwork?template-categories.yaml. Each folder defines its own categories. No central categories file.What happens to— Merged intodev-template-ai?dev-template. App, AI, and doc templates all run in the devcontainer — no reason for separate commands. Only UIS templates need a separate command because they run in a different container.
Decided Architecture: One Repo, Two Installers
The Model
helpers-no/dev-templates (this repo)
│
│ Devcontainer templates (dev-template)
├── templates/ # App templates
├── ai-templates/ # AI workflow templates
├── doc-templates/ # Documentation templates (future)
├── rules-templates/ # Coding rules/linting (future)
│
│ UIS templates (uis template)
├── uis-infra-templates/ # Infrastructure service templates (future)
├── uis-stack-templates/ # Multi-service stack compositions (future)
│
│ Shared
├── scripts/ # Generation and validation scripts
└── website/ # Docusaurus shows everything
Both sides use subfolders for the same reason — we don't know what the future brings. New template types get their own folder and category without restructuring.
Two Execution Contexts
| Devcontainer Templates | UIS Templates | |
|---|---|---|
| Folders | templates/, ai-templates/, doc-templates/, rules-templates/ | uis-infra-templates/, uis-stack-templates/ |
| Runs in | Devcontainer (DCT) | provision-host (UIS) |
| Command | dev-template | uis template (or similar) |
| Script lives in | DCT repo (devcontainer-toolbox) | UIS repo (urbalurba-infrastructure) |
| Has access to | Project workspace, npm, code editors | kubectl, helm, Ansible, UIS services |
| Installs | Files into user's project | Services into K8s cluster |
| Sparse-checkout | Folders where CONTEXT=dct in TEMPLATE_FOLDERS | Folders where CONTEXT=uis in TEMPLATE_FOLDERS |
| Post-install | dev-template configure reads YAML, installs tools, calls UIS | uis configure creates databases, exposes ports |
Generated Registry: template-registry.json
All template metadata is assembled into one JSON file at website/src/data/template-registry.json. This is auto-generated from template-categories.yaml + template-info.yaml files — never hand-edited. It replaces the current templates.json + categories.json with a single file that serves both Docusaurus and installer scripts.
{
"categories": [
{
"id": "BASIC_WEB_SERVER",
"order": 0,
"name": "Basic Web Server Templates",
"description": "Minimal web server templates that demonstrate Hello World in multiple languages",
"tags": "webserver backend hello-world starter",
"logo": "webserver-logo.svg",
"emoji": "🌐",
"context": "dct"
},
{
"id": "WEB_APP",
"order": 1,
"name": "Web Application Templates",
"description": "Frontend web application starter templates",
"tags": "webapp frontend react vite",
"logo": "webapp-logo.svg",
"emoji": "📱",
"context": "dct"
},
{
"id": "WORKFLOW",
"order": 2,
"name": "Workflow Templates",
"description": "AI-assisted development workflow templates",
"tags": "ai workflow planning automation",
"logo": "workflow-logo.svg",
"emoji": "🤖",
"context": "dct"
}
],
"templates": [
{
"id": "python-basic-webserver",
"folder": "templates/python-basic-webserver",
"version": "1.0.0",
"name": "Python Basic Webserver",
"description": "Minimal Flask server with health endpoint and Docker support",
"category": "BASIC_WEB_SERVER",
"abstract": "Provides a minimal starting point for developers who want to build a Python web server using Flask",
"tools": "dev-python",
"readme": "README-python-basic-webserver.md",
"tags": "python flask webserver api rest",
"logo": "python-basic-webserver-logo.svg",
"website": "",
"docs": "https://github.com/helpers-no/dev-templates/tree/main/templates/python-basic-webserver",
"summary": "A minimal Python web server using Flask...",
"related": "php-basic-webserver typescript-basic-webserver"
}
]
}
Two-Step Flow
Step 1: Fetch registry (one curl, small JSON file)
curl -sL "https://tmp.sovereignsky.no/data/template-registry.json" > /tmp/registry.json
Step 2: Build menu from registry (no git operations yet)
# Get all dct categories
jq -r '.categories[] | select(.context == "dct") | "\(.emoji) \(.name)"' /tmp/registry.json
# Get templates for a category
jq -r '.templates[] | select(.category == "BASIC_WEB_SERVER") | .name' /tmp/registry.json
The user can browse categories, read descriptions, go back and forth — all from this one file. No downloads, no sparse-checkout.
Step 3: Sparse-checkout only the selected template (after user picks)
# User selected python-basic-webserver
folder=$(jq -r '.templates[] | select(.id == "python-basic-webserver") | .folder' /tmp/registry.json)
git sparse-checkout set "$folder"
Only the chosen template's folder is downloaded. Fast, minimal.
1DCT: This two-step flow (curl registry → sparse-checkout only selected template) is a significant improvement over the current approach. Today we sparse-checkout the entire
templates/orai-templates/folder upfront just to show the menu. Fetching a small JSON registry first means faster browsing and smaller downloads. The menu can be instant — no git operations until the user confirms a selection.
Decided File Architecture
| File | Location | Maintained | Purpose |
|---|---|---|---|
template-categories.yaml | Per template type folder | By hand (YAML) | Folder context, name, description, category definitions |
template-info.yaml | Per template | By hand (YAML) | Template metadata (replaces current TEMPLATE_INFO bash format) |
template-registry.json | website/src/data/template-registry.json | Auto-generated (TypeScript/CI) | Single JSON consumed by Docusaurus AND installer scripts |
template-categories.yaml (per folder, maintained by hand):
# template-categories.yaml — category definitions for this template folder
context: dct
name: App Templates
description: App project templates (web servers, web apps)
order: 0
emoji: "🌐"
categories:
- id: BASIC_WEB_SERVER
order: 0
name: Basic Web Server Templates
description: Minimal web server templates that demonstrate Hello World
tags: webserver backend hello-world starter
logo: webserver-logo.svg
emoji: "🌐"
- id: WEB_APP
order: 1
name: Web Application Templates
description: Frontend web application starter templates
tags: webapp frontend react vite
logo: webapp-logo.svg
emoji: "📱"
template-info.yaml — see Final template-info.yaml Specification below for the complete field reference.
Quick example (simple app template — no services):
id: python-basic-webserver
version: "1.0.0"
name: Python Basic Webserver
description: Minimal Flask server with health endpoint and Docker support
category: BASIC_WEB_SERVER
install_type: app
tools: dev-python
readme: README-python-basic-webserver.md
tags: [python, flask, webserver, api, rest]
logo: python-basic-webserver-logo.svg
The category value must match a category defined in the parent folder's template-categories.yaml.
Repo structure:
templates/
├── template-categories.yaml # context=dct, defines BASIC_WEB_SERVER + WEB_APP
├── python-basic-webserver/
│ └── template-info.yaml
└── designsystemet-basic-react-app/
└── template-info.yaml
ai-templates/
├── template-categories.yaml # context=dct, defines WORKFLOW
└── plan-based-workflow/
└── template-info.yaml
uis-stack-templates/ # future
├── template-categories.yaml # context=uis, defines ORGANISATION_STACK
└── red-cross-infrastructure/
└── template-info.yaml
Each folder is self-describing. Adding a new folder = create it + add template-categories.yaml + add templates. No central file to edit.
Generation: A TypeScript script (runs in CI) scans all */template-categories.yaml + */*/template-info.yaml → outputs website/src/data/template-registry.json.
What goes away:
scripts/lib/TEMPLATE_CATEGORIES— replaced bytemplate-categories.yamlper foldertemplates/TEMPLATE_CATEGORIEScopies — no longer neededai-templates/TEMPLATE_CATEGORIEScopies — no longer neededwebsite/src/data/templates.json— merged into registrywebsite/src/data/categories.json— merged into registry- CI sync step for TEMPLATE_CATEGORIES — no longer needed
One Generated File Serves Everything
template-registry.json lives at website/src/data/template-registry.json and serves two consumers:
Docusaurus website:
- React components import it directly — one file instead of two
- Shows all templates from all contexts (dct + uis)
DCT/UIS installer scripts:
- Fetch from the live Docusaurus site:
curl -sL "https://tmp.sovereignsky.no/data/template-registry.json"
- Or fetch from raw GitHub as fallback:
curl -sL "https://raw.githubusercontent.com/helpers-no/dev-templates/main/website/src/data/template-registry.json"
- Parse with
jq, filter by context, build menu — no git operations needed for browsing
Why JSON
- Parsed safely with
jq(already a dependency) - No ambiguity — strict format, no quoting issues
- Imported directly by Docusaurus React components
- Backstage field mapping is straightforward
- No bash sourcing needed
What's Unified
- Same repo — all templates in
helpers-no/dev-templates - Same
template-info.yamlformat — all fields work the same - Same
template-categories.yamlformat — consistent folder declarations - Same
template-registry.json— one generated file for website and all installers - Same Docusaurus website — shows all templates from all contexts
- Same validation —
validate-metadata.shchecks everything
What's Different
- Installer script — DCT has
dev-template, UIS hasuis template - Execution context — devcontainer vs provision-host
- Context filter — each installer shows only its context from the registry
- Configure capabilities — DCT's
dev-template configurehas npm/pip, UIS'suis configurehas kubectl/helm/ArgoCD - Installation target — user's project directory vs K8s cluster
Migration Path
- Create
template-categories.yamlfortemplates/andai-templates/ - Create TypeScript generation script → outputs
website/src/data/template-registry.json - Update CI to run
generate-registry.sh - Update Docusaurus React components to import
template-registry.json - Update
generate-docs-markdown.shto read from registry - Update DCT
dev-template.shto fetch and parse registry withjq - Merge
dev-template.shanddev-template-ai.shinto one script filtering by context - Remove old files:
TEMPLATE_CATEGORIES,templates.json,categories.json - Future: Add
uis templatein UIS repo for infrastructure templates
6DCT: On
jqdependency — the registry-based approach requiresjqto parse JSON in bash.jqis already pre-installed in the DCT base image, so this is not a blocker.7DCT: On the migration path (steps 6-7) — merging the two scripts and switching to registry-based browsing is a complete rewrite of the scanning/menu code in
template-common.sh. Currently DCT sourcesTEMPLATE_CATEGORIESas a bash file and scans directories. Switching tocurl+jqontemplate-registry.jsonreplaces all of that. Not difficult, but needs to happen in coordination with TMP publishing the registry to the live Docusaurus site.
Installation Behaviour Differences
This is the key challenge. Different template types install differently:
| Type | Installation behaviour |
|---|---|
| App templates | Copy all files to project root. Replace {{GITHUB_USERNAME}} and {{REPO_NAME}} in manifests/workflows. |
| AI templates | Copy template/ subdirectory contents preserving directory structure. Handle CLAUDE.md conflict. |
| Infra templates (future) | May need Helm chart installation, K8s apply, or UIS service registration |
| Doc templates (future) | May need npm install, Docusaurus init, or similar setup |
This could be handled by an install_type field in template-info.yaml:
install_type: app # Copy to root, replace placeholders
install_type: overlay # Copy template/ preserving paths, handle conflicts
install_type: custom # Requires dev-template configure for service setup
2DCT: Merging
dev-templateanddev-template-aiis straightforward. The scripts are already 80% shared library. The remaining script-specific logic is:verify_template()(different validation rules),copy_template_files()(different copy strategies),process_template_files()(different placeholder patterns), andcleanup_and_complete()(different completion messages). Withinstall_typeintemplate-info.yaml, these become handlers in the shared library keyed by type.3DCT: On
install_type— the three types proposed (app,overlay,custom) map cleanly to what we already have:
app= currentdev-template.shbehavior (copy to root, replace placeholders, setup workflows, merge gitignore)overlay= currentdev-template-ai.shbehavior (copytemplate/preserving paths, handle CLAUDE.md, safe re-run logic)custom= future, would needdev-template configureas described in the Security section
TMP as the Glue Layer Across Projects
The Pattern
TMP (dev-templates) already serves as the glue between DCT and user projects:
DCT (dev-setup) → installs tools (golang, python, typescript)
TMP (dev-template) → scaffolds a project using those tools
User project → the result
The same pattern could apply to UIS:
UIS (uis deploy) → installs services (postgresql, redis, grafana)
TMP (uis-template?) → scaffolds config/apps that connect to those services
User project → the result, pre-wired to UIS services
What UIS Templates Could Look Like
App + Service compositions:
nextjs-postgres→ Next.js app pre-configured with DATABASE_URL, .env, Prisma schema, migration setupflask-redis→ Flask app with Redis caching, connection code, health checksexpress-rabbitmq→ Express API with RabbitMQ consumer/publisher, message schemas
Infrastructure compositions (service stacks):
observability-stack→ deploys Prometheus + Grafana + Loki with pre-configured dashboards and alertsdata-pipeline→ deploys PostgreSQL + RabbitMQ + worker service with connection wiringai-local-stack→ deploys Ollama + LiteLLM + Open WebUI as a complete local AI setup
Configuration overlays:
add-database→ adds PostgreSQL connection to an existing project (.env, connection code, migration folder)add-monitoring→ adds OTEL instrumentation and Grafana dashboard to an existing appadd-auth→ adds Authentik SSO configuration to an existing app
Where UIS Templates Live — Decided
In dev-templates. All templates (DCT and UIS) live in one repo. Each folder has its own template-categories.yaml with context: uis. The uis template installer fetches the registry, filters by context: uis, and shows only infrastructure templates.
Organisation-Level Infrastructure Templates
Beyond single-service templates, we could describe complete infrastructure stacks for an organisation. For example:
"Red Cross Volunteer Platform" stack:
- PostgreSQL (user database)
- Authentik (SSO/authentication)
- Tailscale (VPN/networking)
- Grafana + Prometheus (monitoring)
- ArgoCD (user app deployment)
A template for this would contain:
- A service list that
uis templatedeploys viauis deploy(Ansible playbooks — see Decision #11) - Init files for post-deploy configuration (create databases, configure SSO providers, set up dashboards)
.envfiles and connection details for app templates to consume- Documentation about what was deployed and how to manage it
Deployment Engine Decision
1UIS: Decided: Ansible-based deployment, no ArgoCD GitOps for infrastructure.
UIS uses Ansible playbooks as the deployment engine —
uis deploy <service>runs Ansible which doeshelm upgrade --install, waits for pods, runs health checks. ArgoCD is deployed as a service but is only used for user app deployment (uis argocd registerpoints ArgoCD at a user's GitHub repo). The UIS infrastructure itself is not managed by ArgoCD in GitOps mode.Stack templates will use the existing Ansible deployment:
uis templatereads the template's service list and callsuis deploy+uis configurefor each service. No ArgoCD ApplicationSets needed.ArgoCD GitOps for infrastructure management (ArgoCD watching the UIS repo and auto-reconciling) is out of scope for this project. It may be explored as a separate future project.
Real Example: "Red Cross Infrastructure" Template
A UIS template that deploys and configures an entire organisation's platform:
uis-stack-templates/
├── template-categories.yaml # context=uis, defines ORGANISATION_STACK category
└── red-cross-infrastructure/
├── template-info.yaml # category=ORGANISATION_STACK, lists services to deploy
├── README-red-cross-infrastructure.md
├── config/ # Init files for uis configure
│ ├── authentik-providers.yaml
│ ├── grafana-dashboards.json
│ └── postgresql-databases.sql
└── (no scripts — uis template orchestrates via uis deploy + uis configure)
User flow:
1. uis template
→ picks "Red Cross Infrastructure"
→ sparse-checkout just this template folder
2. uis template reads template-info.yaml in provision-host:
→ calls uis deploy for each service (runs Ansible playbooks — see 1UIS)
→ calls uis configure for each service that needs app-specific setup
→ returns connection details as JSON
3. Result: complete Red Cross platform running in K8s
(deployed via Ansible — see Decision #11)
Then a developer scaffolds an app on top:
4. dev-template (in devcontainer)
→ picks "Next.js Volunteer App"
→ dev-template configure reads template-info.yaml
→ calls uis-bridge for each required service
→ generates .env with DATABASE_URL, AUTH_URL, etc.
5. Result: app scaffolded and pre-wired to the running infrastructure
This is the full producer/consumer chain:
- UIS template produces the infrastructure (databases, auth, monitoring) via
uis deploy(Ansible) - DCT template consumes it (app wired to those services via
uis configure)
On the website:
🏢 Organisation Stacks (uis)
- Red Cross Infrastructure
- (future: other org stacks)
🌐 Basic Web Server Templates (dct)
- Python Basic Webserver
- TypeScript Basic Webserver
📱 Web Application Templates (dct)
- Next.js Volunteer App (consumes Red Cross Infrastructure)
The same template system handles everything — from a simple Hello World webserver to a complete organisational infrastructure. Same template-info.yaml format, same template-registry.json, same Docusaurus website, same validation.
The Connection Problem — Detailed Analysis
The key challenge: a DCT template running in the devcontainer needs database connection details, but only UIS (running in the provision-host container) has access to kubectl, Helm, manifests, Ansible playbooks and the credentials.
UIS secrets architecture (from website/docs/contributors/architecture/secrets.md):
- All services share credentials through one Kubernetes Secret (
urbalurba-secrets) - Development defaults are preset so services run immediately — standard userids/passwords
- Credentials are defined in
.uis.secrets/secrets-config/00-common-values.env.template - The provision-host generates and applies secrets:
./uis secrets generate→./uis secrets apply
Note: UIS can always just read the current secrets in the cluster using kubectl
2UIS: Confirmed accurate. The secrets system uses
envsubstto render00-master-secrets.yml.templatewith variables from00-common-values.env.template. The generatedkubernetes-secrets.ymlcreatesurbalurba-secretsin all namespaces. Key detail: all services currently share a singleDEFAULT_DATABASE_PASSWORD. Foruis configureto create per-app credentials (which it must — we can't hand out admin passwords to every app), UIS uses the admin credentials fromurbalurba-secretsinternally to connect to the service, then creates the app-specific database/user/password directly in the running service (e.g.,CREATE USER ... CREATE DATABASE ...in PostgreSQL). The per-app credentials are returned in the JSON output to DCT — UIS does not store them. It is DCT's responsibility to persist them (e.g., in.env). The UIS secrets system (urbalurba-secrets) is not involved in per-app credentials at all — it only provides the admin access thatuis configureuses internally.
The gap:
DCT devcontainer UIS provision-host
├── No kubectl ├── Has kubectl
├── No Helm ├── Has Helm
├── No K8s access ├── Has K8s access
├── Needs DATABASE_URL ├── Knows credentials
├── Needs to create a database ├── Can create databases
└── Runs dev-template └── Runs uis deploy / uis template
The two-step problem:
- Deploy the service —
uis deploy postgresql(runs in uis-provision-host, deploys PostgreSQL to K8s with preset credentials) - Configure for the app — create a specific database + user for this app, generate
.envwith connection details
Step 1 is UIS's job. Step 2 is where the bridge is needed — who creates the database and how does the connection info get to the devcontainer?
Proposed solution: uis configure command
A new UIS command that dev-template configure calls via uis-bridge (see 3UIS):
# In provision-host:
uis configure postgresql --app volunteer-app --database volunteer_db
This would:
- Connect to the running PostgreSQL (using known preset credentials)
- Create the database
volunteer_dband a user for it - Write connection details to a shared location that the devcontainer can read
3UIS: The
docker execbridge approach is sound. However, DCT should not calldocker execdirectly in its code. Instead, encapsulate the communication in a wrapper script (e.g.,uis-bridge.shin DCT) that handles container discovery,docker exec, stdin piping for init files, and error handling. All DCT code calls the bridge, neverdocker execdirectly.This way, if we later change the communication method (e.g., REST API, Unix socket, shared volume, or UIS running as a sidecar), only the bridge script changes — no rewrites in
dev-template configureor any template logic.# DCT calls this (abstracted):uis-bridge configure postgresql --app volunteer-app --json# The bridge script internally does (today):docker exec uis-provision-host uis configure postgresql --app volunteer-app --jsonUIS will ensure the
uiscommand is available at a well-known path inside the container.UIS internal note: add
RUN ln -s /mnt/urbalurbadisk/provision-host/uis/manage/uis-cli.sh /usr/local/bin/uistoDockerfile.uis-provision-host.
Decided approach: DCT uses uis-bridge to run commands in UIS container
DCT communicates with UIS through uis-bridge.sh (see 3UIS). The bridge abstracts the transport — today it uses docker exec, but this can change without affecting any calling code.
# In DCT devcontainer, dev-template configure internally does:
# Call UIS via the bridge (never docker exec directly)
CONNECTION_INFO=$(uis-bridge configure postgresql --app volunteer-app --json)
# Parse and write .env
echo "DATABASE_URL=$(echo $CONNECTION_INFO | jq -r '.database_url')" >> .env
What's needed:
| Task | Who | Status |
|---|---|---|
Docker CLI install script (see DCT website/docs/contributors/adding-tools) | DCT maintainer | Not started |
| Docker socket mount in devcontainer.json | DCT | Already configured |
uis configure command that creates databases and outputs connection info as JSON | UIS maintainer | Not started |
Templates declare requires in template-info.yaml | TMP | Format defined |
The flow:
1. User picks "Next.js Volunteer App" in dev-template → files copied to project
2. User runs: dev-template configure
3. dev-template configure reads template-info.yaml, sees requires: postgresql
4. dev-template configure runs: uis-bridge configure postgresql --app volunteer-app --json
5. UIS creates database, returns connection details as JSON
6. dev-template configure writes .env with DATABASE_URL, etc.
7. App is scaffolded and pre-wired
What this means for templates:
A DCT template's template-info.yaml would declare:
requires:
- service: postgresql
config:
database: "{{app_name}}_db"
Benefits:
- Clean separation — DCT doesn't need kubectl/Helm, just Docker CLI
- UIS handles all K8s/service logic
- The
uis-bridgeabstraction keeps DCT decoupled from the transport mechanism - No shared filesystems needed between containers
- Connection details flow through stdout/JSON — simple and parseable
4UIS: On the
requireslifecycle —uis deployis idempotent. Both Ansible (helm upgrade --install) andkubectl applyare safe to re-run when a service is already running. So DCT/TMP could simply always calluis deploy postgresqlbeforeuis configure postgresql. However, a deploy on an already-running service still takes 30-60 seconds (runs through all playbook tasks and health checks even when nothing changes).Recommendation:
uis configureshould handle this itself — check if the service is running first (fast kubectl check), and if not, return a clear error with the deploy command. This keeps the fast path fast (service already running → straight to configure) while avoiding silent slow deploys. If DCT wants to pre-check,uis status <service> --jsoncan return the running state before presenting the template to the user.UIS internal note: consider adding a fast-path to
uis deploythat skips the full playbook when the service is already healthy — would make the "always deploy first" pattern viable.
Two Environments: Development vs In-Cluster
An app like "Next.js + PostgreSQL" needs to reach PostgreSQL in two different environments:
1. Local development (developer running app in DCT devcontainer):
DCT container → host.docker.internal:<port> → Host machine → K8s PostgreSQL
2. In-cluster (app built and deployed by ArgoCD):
Next.js pod → postgresql.default.svc.cluster.local:5432 → K8s internal DNS
The template generates both connection configs:
# .env.development (used locally in DCT devcontainer)
DATABASE_URL=postgresql://user:pass@host.docker.internal:5432/volunteer_db
# In-cluster connection (in K8s deployment manifest / Secret)
DATABASE_URL=postgresql://user:pass@postgresql.default.svc.cluster.local:5432/volunteer_db
host.docker.internal is a Docker DNS name that resolves to the host machine from inside any Docker container. It works on macOS, Windows, and Linux (Docker 20.10+).
The key requirement: PostgreSQL's port must be reachable from the DCT container. This is a UIS responsibility — see requirements below.
5UIS: Confirmed —
host.docker.internalis the correct approach. I've verified the networking:
- UIS provision-host runs with
--network host(shares host network stack directly)- DCT devcontainer runs on Docker's default bridge network (no
--network hostin itsrunArgs)- Therefore DCT cannot use
localhostto reach the host — it must usehost.docker.internal- Any port UIS exposes on the host (via NodePort or port-forward) is reachable from DCT via
host.docker.internal:<port>- Since UIS uses
--network host, port-forwards inside the provision-host bind directly to the host machine — no extra hops needed
Future: Remote K8s Clusters
Today DCT, UIS, and K8s (k3s via Rancher Desktop) all run locally on the developer's machine. But in the future K8s may run elsewhere:
Today:
Developer's machine
├── DCT devcontainer
├── UIS provision-host
└── Rancher Desktop (k3s) ← K8s is local
Future:
Developer's machine
├── DCT devcontainer
└── UIS provision-host (has kubectl to remote cluster)
Remote (Azure, another machine, cloud)
└── K8s cluster ← K8s is remote
UIS already handles this through kubeconfig — uis-provision-host connects to whatever cluster is configured in .uis.secrets/generated/kubeconfig/kubeconf-all. The uis deploy and uis configure commands work the same regardless of where K8s runs.
Impact on the connection problem:
The only guaranteed path from DCT to K8s services is through UIS. DCT cannot reach the remote K8s directly — there's no kubectl, no firewall access, no VPN. Only UIS has the connection.
This means UIS must proxy the connection for DCT:
DCT container
→ uis-bridge expose postgresql
→ UIS runs kubectl port-forward (it has kubectl access to any cluster)
→ UIS binds the port locally
→ DCT connects via host.docker.internal:<port>
The pattern is the same regardless of whether K8s is local or remote:
- DCT asks UIS to expose a service (via
uis-bridge) - UIS handles the connection internally (NodePort for local, port-forward for remote)
- UIS makes the service reachable on a local port
- DCT connects — it never needs to know where K8s actually runs
This is the critical insight: DCT's only connection to K8s is through UIS. Templates should never assume direct K8s access. The uis expose and uis configure commands abstract away the cluster location completely.
One path, always:
Development: DCT → uis-bridge → UIS → kubectl → K8s (local or remote)
In-cluster: App pod → K8s internal DNS → Service (always the same)
No special cases for local vs remote. UIS always proxies the connection. DCT always connects the same way. This keeps templates simple — they don't need to know or care where K8s runs.
Requirements for UIS Contributor
This section summarises what TMP and DCT need from UIS to make service-dependent templates work.
1. Service port exposure for local development
When a developer runs an app locally in the DCT devcontainer, the app needs to reach K8s services (PostgreSQL, Redis, etc.) via host.docker.internal:<port>.
This means UIS must expose service ports to the host machine. Options (UIS decides which):
- NodePort — K8s Service type NodePort exposes the service on a high port on the host. Persistent, survives restarts.
- Traefik TCP routing — Traefik can route TCP connections (not just HTTP). Requires adding TCP entrypoints.
- kubectl port-forward — forwards a pod port to the host. Simple but needs a running process.
6UIS: Current state — all services use ClusterIP (except JupyterHub which uses NodePort). My recommendation is a hybrid approach:
- Default:
kubectl port-forward— simplest to implement, works for both local and remote clusters, and since UIS runs with--network hostthe forwarded port is immediately available on the host machine. Theuis exposecommand would manage the port-forward process (start/stop/status).- Persistent option: NodePort — for services the developer wants always available (survives restarts).
uis expose postgresql --persistentcould patch the service to NodePort.- Traefik TCP routing — I'd defer this. It adds complexity (TCP entrypoint configuration, SNI routing) and doesn't solve a problem the other two options don't already handle.
Port assignment: I propose fixed well-known port mappings per service to keep things predictable. For example: PostgreSQL → 35432, Redis → 36379, MongoDB → 37017, MySQL → 33306. These avoid conflicts with any local installations on standard ports. The mappings would live in UIS's
services.json(new field:exposePort).uis configurewould return the actual port in its JSON output regardless of approach used.
Requirement: The developer should be able to easily enable/disable/check the connection. Something like:
# In provision-host:
uis expose postgresql # make PostgreSQL reachable from host
uis expose postgresql --stop # stop exposing
uis expose --status # show what's currently exposed
2. uis configure command
A command that creates app-specific resources in a deployed service and returns connection details as JSON:
# In provision-host:
uis configure postgresql --app volunteer-app --database volunteer_db --json
Success output:
{
"status": "ok",
"service": "postgresql",
"local": {
"host": "host.docker.internal",
"port": 35432,
"database_url": "postgresql://volunteer_app:pass@host.docker.internal:35432/volunteer_db"
},
"cluster": {
"host": "postgresql.default.svc.cluster.local",
"port": 5432,
"database_url": "postgresql://volunteer_app:pass@postgresql.default.svc.cluster.local:5432/volunteer_db"
},
"database": "volunteer_db",
"username": "volunteer_app",
"password": "generated-password"
}
Error output (Decision #13):
{
"status": "error",
"phase": "init_file",
"service": "postgresql",
"detail": "ERROR: syntax error at or near \"CREAT\" at line 12"
}
status—"ok"on success,"error"on failure. DCT checks this first.phase— where the error occurred:"deploy_check"(service not running),"create_resources"(database/user creation failed),"init_file"(init file application failed),"expose"(port exposure failed).detail— raw error message from the service tool. DCT shows this to the developer.local— UIS determines the host and port. The port is whatever UIS has exposed (could be 5432 if available, or a high port like 35432). DCT doesn't choose the port — UIS does.cluster— standard K8s internal DNS, always the same pattern.- Human-readable progress on stderr. Exit code non-zero on failure.
The template uses local for .env.development and cluster for the K8s deployment manifest.
7UIS: The JSON output format looks good. Here is the full
uis configureinterface as UIS understands it from therequiresblock intemplate-info.yaml:Command interface:
uis configure <service> --app <app-name> [--database <db-name>] [--init-file <path>] --json
<service>— must match a UIS service ID fromservices.json(e.g.,postgresql,redis,authentik)--app— the app name, used to derive username (e.g.,volunteer-app→ uservolunteer_app)--database— service-specific: database name for PostgreSQL/MySQL/MongoDB, key prefix for Redis, etc.--init-file— optional data file (SQL, YAML, JSON) to apply after creating resources. UIS applies it using the appropriate service tool (psql -ffor PostgreSQL, API calls for Authentik/Grafana). The file is data, not executable code — UIS validates and applies it safely.--json— return connection details as JSON on stdout. Human-readable progress on stderr.What UIS does internally:
- Check service is deployed (fast kubectl check — fail with helpful error if not, see 4UIS)
- Read admin credentials from
urbalurba-secrets- Generate per-app password
- Create database/user/resources in the running service
- Apply init file if provided
- Auto-expose the service port if not already exposed (see 6UIS)
- Return JSON with
localandclusterconnection detailsIdempotency: Running
uis configuretwice with the same args must not fail — it detects existing resources and returns the current connection details. However, note that on a second run, the generated password from the first run is lost (UIS doesn't store it, see 2UIS). UIS would need to either: (a) reset the password to a new one and return it, or (b) detect "already configured" and return a message telling DCT to use its stored credentials. Option (b) is safer — avoids invalidating a working.env.Per-service handlers: Each configurable service needs its own handler in
provision-host/uis/lib/configure-<service>.sh. Start with PostgreSQL, then Redis, MongoDB, MySQL, Authentik.UIS internal note: the
--init-filepath is tricky — the file lives in DCT's filesystem, not inside the UIS container. DCT will need to pipe the file content via stdin or usedocker execwith-iflag:docker exec -i uis-provision-host uis configure postgresql --init-file - < config/init-database.sql
3. Docker CLI in DCT
DCT needs a Docker CLI install script so that uis-bridge.sh can communicate with the UIS provision-host container. The Docker socket is already mounted in the DCT devcontainer.json. See DCT website/docs/contributors/adding-tools for how to create install scripts.
5DCT: DCT does NOT currently have a Docker CLI install script. The Docker socket is mounted (
/var/run/docker.sock) but thedockercommand is not available inside the devcontainer. We need to createinstall-tool-docker-cli.sh. This is a prerequisite for any template thatrequiresUIS services. Note: this is a static binary install (similar to how Hugo is installed) — no apt repository needed.
4. UIS container discovery
DCT needs a reliable way to find the UIS uis-provision-host container. Options:
- Fixed container name (UIS sets
container_name: uis-provision-hostin its Docker Compose) - Label-based discovery (
docker ps --filter "label=uis.role=provision-host") - Image-based discovery (
docker ps --filter "ancestor=ghcr.io/helpers-no/uis-provision-host")
8UIS: Use the fixed container name
uis-provision-host. This is already hardcoded in the./uishost wrapper script and is the only container name UIS uses. It's the simplest and most reliable option. No labels are currently set on the container, and image-based discovery is fragile (the image can be overridden viaUIS_IMAGEenv var, or locally built asuis-provision-host:local).DCT should use:
docker exec uis-provision-host uis <command>(see 3UIS for symlink proposal)To check if UIS is running:
docker ps --filter "name=uis-provision-host" --format '{{.Status}}'
5. Service names must match UIS service IDs
When a template declares requires: postgresql, that name must match the UIS service ID exactly. The canonical service IDs are defined in UIS at website/src/data/services.json.
Online at: https://raw.githubusercontent.com/helpers-no/urbalurba-infrastructure/refs/heads/main/website/src/data/services.json
Current services:
litellm, openwebui, jupyterhub, openmetadata, spark, unity-catalog,
elasticsearch, mongodb, mysql, postgresql, qdrant, redis, authentik,
enonic, gravitee, rabbitmq, argocd, backstage, nextcloud, nginx,
pgadmin, redisinsight, whoami, cloudflare-tunnel, tailscale-tunnel,
grafana, loki, otel-collector, prometheus, tempo
Templates must use these exact IDs — no aliases, no abbreviations. The validation script should check that requires values match known UIS service IDs (fetched from UIS services.json or a cached copy).
9UIS: Confirmed —
services.jsonis the canonical source for service IDs. Each service entry also includesnamespace,requires(inter-service dependencies), andhelmChartfields thatuis configurewill use internally. Note that not all services inservices.jsonare "configurable" in theuis configuresense — only data services (postgresql, mysql, mongodb, redis, elasticsearch, qdrant) and identity services (authentik) would have configure handlers. Services like grafana, prometheus, argocd are infrastructure that apps don't connect to directly. We should add aconfigurable: true/falsefield toservices.jsonso TMP's validation can distinguish between services you canrequiresand services you can't.
Summary of UIS tasks:
| Task | Description | UIS Status |
|---|---|---|
| Service port exposure | Expose K8s service ports to host machine so DCT containers can reach them via host.docker.internal | Not started — recommend hybrid port-forward + NodePort approach |
uis expose command | Enable/disable/check service port exposure | Not started — fits into existing CLI command structure |
uis configure command | Create app-specific databases/users, return connection details as JSON | Not started — start with PostgreSQL, expand to other data services |
| Container naming/labels | Make the provision-host container reliably discoverable by DCT | Already done — fixed name uis-provision-host in host wrapper |
configurable field in services.json | Mark which services support uis configure | Not started — needed for TMP validation |
Fixed port mappings in services.json | Add exposePort field for predictable port assignments | Not started |
| Init file format docs | Document the native format for each configurable service's init files, with links to upstream docs (10UIS) | Not started — needed for template authors |
Security: No Executable Scripts in Templates
The Problem
Running a post-install.sh script from a downloaded template means executing untrusted code inside the developer's container. Templates are open — anyone can contribute. A malicious post-install.sh could steal credentials, modify files, or compromise the development environment.
The Decision
Templates contain only data — never executable code.
template-info.yaml— YAML declarations of what's neededtemplate-categories.yaml— YAML category definitions- Template files — source code, configs, manifests (copied, not executed)
All executable logic lives in trusted code maintained by the DCT and UIS platform teams:
| Action | Who executes | Where the code lives |
|---|---|---|
| Copy template files | DCT dev-template | DCT repo (trusted) |
| Install dev tools | DCT dev-template configure | DCT repo (trusted) |
| Deploy K8s services | UIS uis deploy / uis configure | UIS repo (trusted) |
| Expose service ports | UIS uis expose | UIS repo (trusted) |
| Create databases/users | UIS uis configure | UIS repo (trusted) |
How It Works
The template author declares what is needed in template-info.yaml:
install_type: app
tools: dev-typescript
requires:
- service: postgresql
config:
database: volunteer_db
After template files are copied, the developer runs a trusted DCT command:
dev-template configure
This trusted command reads template-info.yaml and:
- Reads
tools: dev-typescript→ installs TypeScript (DCT code) - Reads
requires: postgresql→ callsuis-bridge configure postgresql --json(UIS code via bridge) - Writes
.envwith connection details returned by UIS
The template author never writes executable code. The DCT and UIS maintainers implement the handlers for each install_type and service type.
4DCT: On the Security decision (no executable
post-install.shin templates) — I agree. Currently neither template script runs any code from the downloaded templates. All logic is in DCT's trusted code. Thedev-template configurecommand proposed here is the right way to handle infrastructure setup without executing untrusted code.
Parameters: Edit YAML Before Configure
Inspired by Backstage's parameters concept, template-info.yaml can include a params section with fields the developer fills in before running dev-template configure.
After dev-template copies files to the project, the developer sees:
# template-info.yaml — edit params below, then run: dev-template configure
id: nextjs-volunteer-app
install_type: app
tools: dev-typescript
params:
app_name: "" # Required — your app name
database_name: "" # Required — database to create in PostgreSQL
auth_provider: "" # Optional — authentik, or leave empty for no auth
requires:
- service: postgresql
config:
database: "{{ params.database_name }}"
The developer edits the YAML:
params:
app_name: "volunteer-app"
database_name: "volunteer_db"
auth_provider: "authentik"
Then runs dev-template configure. The trusted DCT command:
- Reads
template-info.yaml - Validates all required params are filled — if not:
❌ Missing required parameters in template-info.yaml:params.app_name — your app nameparams.database_name — database to createEdit template-info.yaml and run dev-template configure again.
- Substitutes
{{ params.* }}references inrequiresand other fields - Proceeds with tool installation and service configuration
Benefits:
- Developer controls the values — no surprises
- Required fields are validated before any actions run
- The YAML file is reviewable and editable — not a black box
- Same pattern works in CI/CD — fill params from environment variables
- No interactive prompts needed (but could be added as convenience)
- Two paths (see 3MSG): developers edit YAML then run
dev-template configure; CI/CD passes params as CLI args:dev-template configure --param app_name=volunteer-app --param database_name=volunteer_db
Init Files: Templates Reference Data Files for Service Configuration
Templates can include data files (SQL, YAML, JSON) that UIS applies during uis configure. The template-info.yaml references them via an init field:
requires:
- service: postgresql
config:
database: "{{ params.database_name }}"
init: "config/init-database.sql"
- service: authentik
config:
provider: "{{ params.app_name }}"
init: "config/authentik-setup.yaml"
The template folder contains the data files:
nextjs-volunteer-app/
├── template-info.yaml
├── config/
│ ├── init-database.sql # CREATE TABLE, seed data, indexes
│ ├── authentik-setup.yaml # Users, groups, OAuth app, permissions
│ └── grafana-dashboards.json # Pre-built monitoring dashboards
├── app/
│ └── ...
dev-template configure passes the file to UIS via uis-bridge:
uis-bridge configure postgresql \
--app volunteer-app \
--database volunteer_db \
--init-file config/init-database.sql \
--json
UIS applies the file using the appropriate tool for each service:
- PostgreSQL →
psql -f init-database.sql - Authentik → API calls from the YAML definition
- Grafana → dashboard import API from JSON
Security: Init files are data — not executable code. SQL, YAML, and JSON are processed by the trusted UIS service handlers. The template author writes data definitions, UIS decides how to apply them safely.
Examples of init files:
| Service | Init file | What it does |
|---|---|---|
| PostgreSQL | init-database.sql | Create tables, seed data, create indexes |
| Authentik | authentik-setup.yaml | Create OAuth app, users, groups, permissions |
| Grafana | grafana-dashboards.json | Import pre-built dashboards |
| Redis | redis-init.yaml | Create namespaces/key prefixes |
| RabbitMQ | rabbitmq-setup.yaml | Create queues, exchanges, bindings |
Responsibility Split
| Who | Maintains | Does |
|---|---|---|
| Template author | template-info.yaml (data only) | Declares what the template needs |
| DCT maintainer | dev-template configure command | Reads YAML, installs tools, calls UIS |
| UIS maintainer | uis configure, uis expose commands | Creates databases, exposes ports, returns connection JSON |
8DCT: On
dev-template configure— this is a new command that doesn't exist yet. It would readtemplate-info.yamlfrom the project directory (left behind after template install), checkrequires, and calluis-bridge configure .... This is Phase 2 work and depends on UIS havinguis configureready. Thetemplate-info.yamlfile must be kept in the project after install (currentlyTEMPLATE_INFObash files are copied but not used post-install).
Backstage Comparison and Ideas
Backstage (Spotify's open-source developer portal) solves many of the same problems. Comparing what we have, what Backstage does, and what ideas we can adopt.
What's Similar
| Our System | Backstage Equivalent |
|---|---|
template-info.yaml metadata | template.yaml — declarative metadata describing the template |
template-categories.yaml | Software Catalog kind: Template with categories/tags |
dev-template dialog menu | Backstage Scaffolder UI — user picks template, fills parameters |
| Template file copying + placeholder replacement | Scaffolder fetch:template action with Nunjucks templating |
tools (auto-install) | Scaffolder custom actions — run post-scaffold steps |
| Docusaurus template cards/pages | Backstage Software Catalog — browsable catalog of templates |
readme (completion message) | Scaffolder task output — shows result and next steps |
What Backstage Has That We Could Use
-
Parameters/Input Forms — Backstage templates define input parameters (project name, language, features) that the user fills in before scaffolding. We only have
{{GITHUB_USERNAME}}and{{REPO_NAME}}. Future templates (database name, API key, etc.) would need this. Could add aparamsfield intemplate-info.yamlor a separateparams.yamlfile per template. -
Multi-Step Actions — Backstage scaffolder runs a pipeline of actions: fetch template, create repo, register in catalog, create CI/CD, notify Slack. Our installation is a single copy step. For infra templates that need
uis deploy+ create database + generate.env, a step pipeline would be useful. Could define astepsfield intemplate-info.yaml. -
Custom Actions — Backstage lets you write custom scaffolder actions (e.g.,
action: create-database). Our equivalent is therequiresfield intemplate-info.yaml— the trusteddev-template configureanduis configurecommands handle the actions. No untrusted code in templates. -
Software Catalog — Backstage registers every scaffolded project in a central catalog with ownership, dependencies, and API docs. We don't track what was scaffolded — templates are fire-and-forget. Not needed now, but useful for organisations managing many projects.
-
Dry Run — Backstage supports running templates in dry-run mode to validate before executing. We have
validate-metadata.shbut no way to preview what a template will do to the user's project. -
Template Versioning — Backstage templates are versioned and users can pick which version. We have
versionbut don't offer version selection — always latest frommain. -
Infrastructure Integration — 2026 Backstage plugins include Pulumi, Terraform, and Kubernetes scaffolder actions for self-service infra provisioning directly from templates. Aligns with our future infra-templates concept.
-
Composability — Backstage lets you chain templates and actions. We could allow
requires: postgresql-setupintemplate-info.yamlto trigger infra templates before app templates.
What We Have That Backstage Doesn't (for our use case)
- Terminal-native — works in any devcontainer via
dialog, no web UI needed - Zero infrastructure — no Backstage server to deploy and maintain
- Git-native — templates stored in a regular Git repo, no catalog database
- Simple — bash scripts, no TypeScript/React/PostgreSQL stack to maintain
- Lightweight — works offline after sparse-checkout, no API calls
Ideas to Adopt (prioritised)
Short term:
dev-template configure— a trusted DCT command that readstemplate-info.yamland performs setup (install tools, call UIS for services). No untrusted scripts in templates.
Medium term:
params— define additional parameters beyond username/repo intemplate-info.yaml. The install script prompts for them viadialogbefore scaffolding.- Composability —
requires: postgresql-setupintemplate-info.yamltriggers prerequisite templates. Infra templates produce.env, app templates consume it.
Long term:
- Step pipeline — ordered steps per template for complex multi-action installations
- Dry run — preview what files will be created/modified before executing
- Backstage compatibility — export templates as Backstage
template.yamlfor organisations that adopt Backstage
Dependency Map and Lightweight Catalog
Backstage has a software catalog that tracks service dependencies and creates dependency maps. We don't have this today, but the need emerges naturally when templates start declaring dependencies:
# template-info.yaml for nextjs-volunteer-app
id: nextjs-volunteer-app
requires:
- postgresql-setup
- authentik-setup
# template-info.yaml for red-cross-infrastructure
id: red-cross-infrastructure
provides:
- postgresql
- authentik
- tailscale
- grafana
Note: the DCT actually have a script that is backstage compatible that build dependencies of the services in UIS. eg authentik needs postgresql
With requires and provides fields, we can build a dependency map:
red-cross-infrastructure ──provides──> postgresql
──provides──> authentik
nextjs-volunteer-app ──requires──> postgresql-setup
──requires──> authentik-setup
This could be:
- Displayed on the Docusaurus website — show which templates depend on which infrastructure
- Used by the installer — when user picks "Next.js Volunteer App", warn if PostgreSQL isn't deployed, or offer to deploy it first
- Visualised as a dependency graph — like Backstage's catalog graph, but generated from
template-info.yamlfields
This doesn't need a running catalog server. The template-registry.json already has all the data — a React component on the website can render the graph from it. The installer can check dependencies at install time.
Not needed now, but the requires and provides fields should be considered when finalising the template-info.yaml format.
Field Name Alignment with Backstage (reference)
We considered aligning our field names with Backstage's template.yaml. Key differences:
| Our field | Backstage field | Decision |
|---|---|---|
id | metadata.name | Keep id — same meaning |
name | metadata.title | Keep name — consistent with DCT, UIS, TMP |
description | metadata.description | Same |
tags | metadata.tags | Same |
category | spec.type | Keep category — richer concept than Backstage's type |
Decision: Keep our own field names for consistency across DCT, UIS, and TMP. Backstage compatibility is handled by the generation script (INVESTIGATE-backstage.md) that maps our fields to Backstage's when generating template.yaml files.
References
- Backstage Software Templates
- Writing Templates
- Top Backstage Plugins for Infrastructure Automation (2026 Edition)
- Backstage Software Templates GitHub
- Backstage in Production: From Developer Portal to Platform Operating System
- Backstage - The Ultimate Guide 2026
- The Backstage Scaffolder, a Powerful New Orchestration Tool
Final template-categories.yaml Specification
One file per template type folder. Defines the folder's execution context and the categories available within it.
Folder-Level Fields
| # | Field | Type | Required | Current equivalent | Description |
|---|---|---|---|---|---|
| 1 | context | string | Yes | (new) | Execution context. Values: dct (devcontainer — dev-template) or uis (provision-host — uis template). Determines which installer handles templates in this folder. |
| 2 | name | string | Yes | (new) | Human-readable name for this template folder. Shown as a top-level grouping on the website and in menus. |
| 3 | description | string | Yes | (new) | One-line description of this template folder. |
| 4 | order | integer | Yes | (new) | Display order among folders. Lower numbers first. Folders with the same context are sorted by this value. |
| 5 | emoji | string | Yes | (new) | Emoji shown next to the folder name in terminal menus. |
| 6 | categories | list | Yes | TEMPLATE_CATEGORY_TABLE | List of category definitions. See below. |
Category Entry Fields
Each entry in the categories list:
| # | Field | Type | Required | Current TEMPLATE_CATEGORIES column | Description |
|---|---|---|---|---|---|
| 1 | id | string | Yes | ID (column 2) | Category identifier. UPPERCASE_UNDERSCORE format. Referenced by template-info.yaml category field. Must be unique across all folders. |
| 2 | order | integer | Yes | ORDER (column 1) | Display order within this folder. Lower numbers first. |
| 3 | name | string | Yes | NAME (column 3) | Human-readable category name. |
| 4 | description | string | Yes | DESCRIPTION (column 4) | Category description, 50–150 characters. |
| 5 | tags | string | Yes | TAGS (column 5) | Space-separated tags for search/filtering. |
| 6 | logo | string | Yes | LOGO (column 6) | Logo filename in website/static/img/categories/. |
| 7 | emoji | string | Yes | EMOJI (column 7) | Emoji shown in terminal menus next to category name. |
Complete Examples
App templates folder (templates/template-categories.yaml):
# template-categories.yaml — category definitions for app templates
context: dct
name: App Templates
description: App project templates (web servers, web apps)
order: 0
emoji: "🌐"
categories:
- id: BASIC_WEB_SERVER
order: 0
name: Basic Web Server Templates
description: Minimal web server templates that demonstrate Hello World in multiple languages
tags: webserver backend hello-world starter
logo: webserver-logo.svg
emoji: "🌐"
- id: WEB_APP
order: 1
name: Web Application Templates
description: Frontend web application starter templates
tags: webapp frontend react vite
logo: webapp-logo.svg
emoji: "📱"
AI workflow templates folder (ai-templates/template-categories.yaml):
# template-categories.yaml — category definitions for AI workflow templates
context: dct
name: AI Workflow Templates
description: AI-assisted development workflow templates
order: 1
emoji: "🤖"
categories:
- id: WORKFLOW
order: 0
name: Workflow Templates
description: AI-assisted development workflow templates
tags: ai workflow planning automation
logo: workflow-logo.svg
emoji: "🤖"
UIS stack templates folder (uis-stack-templates/template-categories.yaml) — future:
# template-categories.yaml — category definitions for UIS stack templates
context: uis
name: Infrastructure Stacks
description: Multi-service infrastructure compositions deployed via UIS
order: 0
emoji: "🏢"
categories:
- id: ORGANISATION_STACK
order: 0
name: Organisation Stacks
description: Complete infrastructure stacks for an organisation or project
tags: infrastructure stack deployment organisation
logo: organisation-stack-logo.svg
emoji: "🏢"
Migration from TEMPLATE_CATEGORIES
| Current (bash pipe-delimited) | template-categories.yaml | Notes |
|---|---|---|
ORDER column | Category order | Same |
ID column | Category id | Same |
NAME column | Category name | Same |
DESCRIPTION column | Category description | Same |
TAGS column | Category tags | Same (stays space-separated) |
LOGO column | Category logo | Same |
EMOJI column | Category emoji | Same |
| (none) | context | New. Folder-level. dct or uis. |
| (none) | Folder name | New. Human-readable folder name. |
| (none) | Folder description | New. One-line folder description. |
| (none) | Folder order | New. Display order among folders. |
| (none) | Folder emoji | New. Emoji for folder-level display. |
Rules
- One
template-categories.yamlper template type folder — no central categories file - Category
idvalues must be unique across all folders (the generation script validates this) - Adding a new template type = create a folder + add
template-categories.yaml+ add templates - CI copies nothing — each folder is self-contained (unlike the current
TEMPLATE_CATEGORIESsync)
What Goes Away
scripts/lib/TEMPLATE_CATEGORIES— replaced by per-folder YAML filestemplates/TEMPLATE_CATEGORIES(CI-copied) — no longer neededai-templates/TEMPLATE_CATEGORIES(CI-copied) — no longer needed- CI sync step that copies
TEMPLATE_CATEGORIESto template folders — no longer needed
Final template-info.yaml Specification
This is the canonical field reference. All contributors should review and confirm.
Field Reference
| # | Field | Type | Required | Current TEMPLATE_INFO equivalent | Description |
|---|---|---|---|---|---|
| 1 | id | string | Yes | TEMPLATE_ID | Must exactly match the directory name. Used as the unique identifier everywhere. |
| 2 | version | string | Yes | TEMPLATE_VER | Semantic version. Quote it: "1.0.0" (YAML would parse 1.0 as a float). |
| 3 | name | string | Yes | TEMPLATE_NAME | Human-readable display name. |
| 4 | description | string | Yes | TEMPLATE_DESCRIPTION | One-line description. Shown in menus and template cards. |
| 5 | category | string | Yes | TEMPLATE_CATEGORY | Must match a category id in the parent folder's template-categories.yaml. |
| 6 | install_type | string | Yes | (new) | How the trusted installer copies files. Values: app, overlay. See below. |
| 7 | abstract | string | Yes | TEMPLATE_ABSTRACT | One-paragraph explanation shown in the template detail page. Supports multi-line YAML (>). |
| 8 | tools | string | No | TEMPLATE_TOOLS | Space-separated DCT tool IDs to auto-install. Empty string or omit if none. |
| 9 | readme | string | Yes | TEMPLATE_README | Filename of the template README (e.g., README-python-basic-webserver.md). |
| 10 | tags | list | Yes | TEMPLATE_TAGS | YAML list of tags for search/filtering. |
| 11 | logo | string | Yes | TEMPLATE_LOGO | Logo filename in website/static/img/templates/. |
| 12 | website | string | No | TEMPLATE_WEBSITE | External website URL. Empty string or omit if none. |
| 13 | docs | string | Yes | TEMPLATE_DOCS | URL to the template's source on GitHub. |
| 14 | summary | string | Yes | TEMPLATE_SUMMARY | Multi-sentence summary for the detail page body and registry. Supports multi-line YAML (>). |
| 15 | related | list | No | TEMPLATE_RELATED | YAML list of related template IDs. Empty list or omit if none. |
| 16 | params | map | No | (new) | Key-value pairs the developer fills in before dev-template configure. See Params section. |
| 17 | requires | list | No | (new) | UIS services this template needs. Each entry has service, optional env_var (39MSG), and config. See Requires section. |
| 18 | provides | list or map | No | (new) | Services this template makes available (UIS stack templates only). Flat list of service IDs, or extended format with stacks + services keys (26MSG). |
install_type Values
| Value | Behaviour | Used by |
|---|---|---|
app | Copy all files to project root. Replace {{GITHUB_USERNAME}} and {{REPO_NAME}} in manifests/workflows. Setup GitHub workflows. Merge .gitignore. | App templates (templates/) |
overlay | Copy template/ subdirectory contents preserving directory structure. Handle file conflicts (e.g., CLAUDE.md merge). Safe to re-run. | AI templates (ai-templates/), doc templates (doc-templates/), rules templates (rules-templates/) |
Future install_type values (e.g., stack for UIS stack templates) will be added when UIS templates are implemented.
params Format
Optional. Only needed for templates that require developer input before dev-template configure.
params:
app_name: "" # Required — your app name (used in database user, service name)
database_name: "" # Required — database to create in PostgreSQL
auth_provider: "" # Optional — authentik, or leave empty for no auth
Rules:
- Keys are simple identifiers (lowercase, underscores)
- Values are strings — empty string means "required, must be filled in"
- Referenced elsewhere in the file as
{{ params.key_name }} - DCT validates that all empty params are filled before running configure
- Two input paths (3MSG): edit YAML for interactive use,
--param key=valueCLI args for CI/CD - Substitution scope (11UIS): DCT substitutes all
{{ params.* }}references in bothtemplate-info.yamlfields and init files before passing anything to UIS. UIS receives fully resolved data — no template syntax.
requires Format
Optional. Only needed for templates that depend on UIS services.
requires:
- service: postgresql # Must match a UIS service ID (configurable: true)
env_var: DATABASE_URL # Optional — env var name written to .env (39MSG)
config:
database: "{{ params.database_name }}"
init: "config/init-database.sql" # Optional — data file applied by UIS
- service: authentik
env_var: AUTH_URL # Optional — defaults to AUTHENTIK_URL
config:
provider: "{{ params.app_name }}"
init: "config/authentik-setup.yaml"
Rules:
servicemust be a UIS service ID fromservices.jsonwithconfigurable: true(see 9UIS)env_var(optional, 39MSG) — the environment variable name DCT writes to.env. Defaults:postgresql/mysql/mariadb/mongodb→DATABASE_URL, others →<SERVICE>_URL(e.g.,REDIS_URL). Override when the framework expects a different name.configfields are service-specific — passed touis configureas argumentsinitreferences a data file relative to the template directory — piped to UIS viauis-bridge{{ params.* }}references are substituted by DCT before calling UIS- Init files must use the native format of the target service (10UIS) — standard SQL for PostgreSQL, Authentik blueprint YAML for Authentik, Grafana dashboard export JSON for Grafana, etc. UIS passes them directly to the service's own tooling with no translation.
provides Format
Optional. Only for UIS stack templates that deploy services. Supports stack references (26MSG) and per-service config (28MSG):
provides:
stacks:
- observability # deploy only — expands from stacks.json
services:
- service: postgresql # deploy + configure
config:
database: "redcross_db"
init: "config/init-database.sql"
- service: redis # deploy only — no config needed
- service: authentik # deploy + configure
config:
init: "config/authentik-blueprint.yaml"
- service: argocd # deploy only
Service entries can be either:
- Plain string (e.g.,
redis) — deploy only, no post-deploy configuration - Object with
service+config— deploy then configure. Sameconfigstructure asrequires(service-specific fields + optionalinitfile)
Rules:
- Service IDs must match UIS
services.json - Stack IDs must match UIS
stacks.json configfields mirror therequiresformat — same keys, sameinitfile handling (28MSG)- Init files use native service formats (10UIS) and are applied by
uis configure - UIS resolves stacks at deploy time — sorts all services by
priorityfield and deploys in correct order - The registry generator expands stacks so the website shows all individual services
- Template authors don't need to think about deploy ordering — it's automatic
- Used for dependency mapping on the website and in the installer
Complete Examples
Simple app template (no services):
# templates/python-basic-webserver/template-info.yaml
id: python-basic-webserver
version: "1.0.0"
name: Python Basic Webserver
description: Minimal Flask server with health endpoint and Docker support
category: BASIC_WEB_SERVER
install_type: app
abstract: >
Provides a minimal starting point for developers who want to
build a Python web server using Flask.
tools: dev-python
readme: README-python-basic-webserver.md
tags:
- python
- flask
- webserver
- api
- rest
logo: python-basic-webserver-logo.svg
website: ""
docs: https://github.com/helpers-no/dev-templates/tree/main/templates/python-basic-webserver
summary: >
A minimal Python web server using Flask with a health check endpoint,
Docker containerization, Kubernetes deployment manifests, and GitHub
Actions CI/CD workflow. Ideal for microservices and API backends.
related:
- php-basic-webserver
- typescript-basic-webserver
AI workflow template (overlay, no services):
# ai-templates/plan-based-workflow/template-info.yaml
id: plan-based-workflow
version: "1.0.0"
name: Plan-Based AI Workflow
description: Structured AI development with plans, phases, and validation
category: WORKFLOW
install_type: overlay
abstract: >
Provides a complete AI-assisted development workflow with investigation
plans, phased implementation, and human-in-the-loop validation.
Includes CLAUDE.md, plan templates, and git safety rules.
tools: ""
readme: README-plan-based-workflow.md
tags:
- ai
- workflow
- planning
- claude
- devcontainer
logo: plan-based-workflow-logo.svg
website: ""
docs: https://github.com/helpers-no/dev-templates/tree/main/ai-templates/plan-based-workflow
summary: >
A structured AI development workflow that guides AI coding assistants
through investigation, planning, and phased implementation. Includes
CLAUDE.md, 6 portable docs, plan templates, and git safety rules.
Designed for human-in-the-loop collaboration.
related: []
App template with service dependencies (future):
# templates/nextjs-volunteer-app/template-info.yaml
id: nextjs-volunteer-app
version: "1.0.0"
name: Next.js Volunteer App
description: Next.js app with PostgreSQL database and Authentik SSO
category: WEB_APP
install_type: app
abstract: >
A full-stack Next.js application pre-wired to PostgreSQL for data
storage and Authentik for authentication. Includes Prisma ORM setup,
migration scaffold, and OAuth configuration.
tools: dev-typescript
readme: README-nextjs-volunteer-app.md
tags:
- typescript
- nextjs
- react
- postgresql
- authentik
- fullstack
logo: nextjs-volunteer-app-logo.svg
website: ""
docs: https://github.com/helpers-no/dev-templates/tree/main/templates/nextjs-volunteer-app
summary: >
A full-stack Next.js application with PostgreSQL database via Prisma ORM,
Authentik SSO integration, Docker containerization, and Kubernetes
deployment manifests. Run dev-template configure to create the database
and wire up authentication.
related:
- typescript-basic-webserver
params:
app_name: ""
database_name: ""
auth_provider: ""
requires:
- service: postgresql
config:
database: "{{ params.database_name }}"
init: "config/init-database.sql"
- service: authentik
config:
provider: "{{ params.app_name }}"
init: "config/authentik-setup.yaml"
Template folder structure with init files:
templates/nextjs-volunteer-app/
├── template-info.yaml
├── README-nextjs-volunteer-app.md
├── config/ # Init files — data only, applied by UIS
│ ├── init-database.sql # PostgreSQL schema + seed data
│ └── authentik-setup.yaml # Authentik OAuth app + users/groups
├── src/ # App source code
├── manifests/ # K8s deployment manifests
├── .github/workflows/ # CI/CD
└── Dockerfile
Example init file — config/init-database.sql:
-- Init file for nextjs-volunteer-app
-- Applied by: uis configure postgresql --init-file -
-- Creates tables and seed data in the app's database
CREATE TABLE IF NOT EXISTS volunteers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(50),
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assignments (
id SERIAL PRIMARY KEY,
volunteer_id INTEGER REFERENCES volunteers(id),
role VARCHAR(100) NOT NULL,
location VARCHAR(255),
start_date DATE,
end_date DATE
);
CREATE INDEX IF NOT EXISTS idx_volunteers_email ON volunteers(email);
CREATE INDEX IF NOT EXISTS idx_assignments_volunteer ON assignments(volunteer_id);
-- Seed data for development
INSERT INTO volunteers (name, email, status) VALUES
('Test User', 'test@example.com', 'active')
ON CONFLICT (email) DO NOTHING;
Example init file — config/authentik-setup.yaml:
# Init file for nextjs-volunteer-app
# Applied by: uis configure authentik --init-file -
# Uses native Authentik blueprint format — see https://docs.goauthentik.io/docs/blueprints/
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: "Volunteer App Test Users"
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# Groups
- model: authentik_core.group
state: present
identifiers:
name: "volunteers"
attrs:
name: "volunteers"
is_superuser: false
- model: authentik_core.group
state: present
identifiers:
name: "coordinators"
attrs:
name: "coordinators"
is_superuser: false
# Test users (3 of 11 — see full set in UIS manifests/073-authentik-1-test-users-groups-blueprint.yaml)
- model: authentik_core.user
state: present
identifiers:
username: "ok1"
attrs:
username: "ok1"
name: "Ola Nordmann"
email: "ok1@urbalurba.no"
password: "Password123"
is_active: true
attributes:
department: "Økonomi og administrasjon"
title: "Økonomi- og administrasjonsmedarbeider"
groups:
- !Find [authentik_core.group, [name, "coordinators"]]
- model: authentik_core.user
state: present
identifiers:
username: "it1"
attrs:
username: "it1"
name: "Erik Larsen"
email: "it1@urbalurba.no"
password: "Password123"
is_active: true
attributes:
department: "IT"
title: "IT Specialist"
groups:
- !Find [authentik_core.group, [name, "coordinators"]]
- model: authentik_core.user
state: present
identifiers:
username: "dist1"
attrs:
username: "dist1"
name: "Bjørn Nilsen"
email: "dist1@urbalurba.no"
password: "Password123"
is_active: true
attributes:
department: "Distriktskontor"
title: "Distriktsmedarbeider"
groups:
- !Find [authentik_core.group, [name, "volunteers"]]
Note: init files are data only and must use the native format of the target service (10UIS). UIS passes them directly to the service's own tooling — no translation layer. PostgreSQL gets standard SQL (psql -f), Authentik gets native blueprint YAML (applied via blueprint API). Template authors should refer to the upstream format documentation for each service. See the Init Files section and 7UIS for details.
UIS stack template (future):
# uis-stack-templates/red-cross-infrastructure/template-info.yaml
id: red-cross-infrastructure
version: "1.0.0"
name: Red Cross Volunteer Platform Infrastructure
description: Complete infrastructure stack for a volunteer management platform
category: ORGANISATION_STACK
install_type: stack
abstract: >
Deploys and configures a complete infrastructure stack for the Red Cross
volunteer management platform, including database, authentication,
networking, and monitoring.
tools: ""
readme: README-red-cross-infrastructure.md
tags:
- infrastructure
- stack
- postgresql
- authentik
- monitoring
- organisation
logo: red-cross-infrastructure-logo.svg
website: ""
docs: https://github.com/helpers-no/dev-templates/tree/main/uis-stack-templates/red-cross-infrastructure
summary: >
Deploys PostgreSQL, Authentik, Tailscale, Grafana, and Prometheus as a
complete platform stack via UIS Ansible playbooks. Includes init files
for database schema, SSO provider configuration, and monitoring dashboards.
related: []
provides:
stacks:
- observability # prometheus, tempo, loki, otel-collector, grafana
services:
- service: postgresql
config:
database: "redcross_db"
init: "config/init-database.sql"
- service: authentik
config:
init: "config/authentik-blueprint.yaml"
- service: tailscale # deploy only
- service: argocd # deploy only
Migration from TEMPLATE_INFO
Direct field mapping — no fields lost, four fields added:
TEMPLATE_INFO (bash) | template-info.yaml (YAML) | Notes |
|---|---|---|
TEMPLATE_ID | id | Same |
TEMPLATE_VER | version | Must be quoted: "1.0.0" |
TEMPLATE_NAME | name | Same |
TEMPLATE_DESCRIPTION | description | Same |
TEMPLATE_CATEGORY | category | Same |
| (none) | install_type | New. app for current app templates, overlay for current AI templates |
TEMPLATE_ABSTRACT | abstract | Same (use > for multi-line) |
TEMPLATE_TOOLS | tools | Same |
TEMPLATE_README | readme | Same |
TEMPLATE_TAGS | tags | Changed from space-separated string to YAML list |
TEMPLATE_LOGO | logo | Same |
TEMPLATE_WEBSITE | website | Same |
TEMPLATE_DOCS | docs | Same |
TEMPLATE_SUMMARY | summary | Same (use > for multi-line) |
TEMPLATE_RELATED | related | Changed from space-separated string to YAML list |
| (none) | params | New. Only for templates needing developer input |
| (none) | requires | New. Only for templates needing UIS services |
| (none) | provides | New. Only for UIS stack templates |
Decisions Made
One command or multiple?— Two commands (dev-templatein DCT,uis templatein UIS) but one unified repo and registryFolder structure?— Multiple folders withtemplate-categories.yamlper folder, categories scoped per folderInstallation behaviour?— No executable scripts in templates (security).install_typeintemplate-info.yamltells the trusted installer how to copy.dev-template configurereads YAML and performs setup using trusted DCT/UIS code.Category management?— Per-folder viatemplate-categories.yaml, no central categories fileData format?— JSON (template-registry.json) generated fromtemplate-categories.yaml+template-info.yamlRegistry location?—website/src/data/template-registry.jsonserves both Docusaurus and installer scriptsHow installers get data?—curlthe registry before any git operations, browse menu from JSON, sparse-checkout only the picked templateDocusaurus structure?— Three levels: context (dct/uis) → category → templateSecurity?— Templates are data only. Nopost-install.sh. All executable logic in trusted DCT/UIS code. Template authors declare what's needed, platform maintainers implement the how.DCT → K8s connection?— Always through UIS:DCT → uis-bridge → UIS → kubectl → K8s. Works for both local and remote clusters.Stack deployment engine?— Ansible (existinguis deployplaybooks). ArgoCD GitOps for infrastructure is out of scope — separate future project if needed.— Always copied to project root by the installer, regardless oftemplate-info.yamlpersistence?install_type. Needed bydev-template configurein Phase B.Init file error handling?— Structured JSON errors from UIS ({"status": "error", "phase": "init_file", ...}), pre-validation where practical (SQLON_ERROR_STOP, YAML lint), DCT wraps with context (file name, service, actionable message).dev-template configureis idempotent — re-run skips succeeded, retries failed.
Next Steps
Aligned with DCT's Phase A/B split (4MSG). Dependency chain: TMP Phase 1 → DCT Phase A → UIS commands → DCT Phase B.
TMP Phase 1 — Registry and metadata (no dependencies)
- Define
template-categories.yamlformat in detail — see Final Specification section - Define
template-info.yamlfinal field list (18 fields includinginstall_type,requires,provides,params) — see Final Specification section - Create
template-categories.yamlfortemplates/andai-templates/ - Create
template-info.yamlfor all existing templates - Create TypeScript generation script →
template-registry.json - Update CI pipeline to run generation
- Update Docusaurus components to use
template-registry.json - Create PLAN for TMP Phase 1
DCT Phase A — Merge scripts, registry browsing (depends on: TMP Phase 1)
- Merge
dev-template.sh+dev-template-ai.shinto one command - Switch to registry-based browsing (
curl+jq) - Support
install_type(app/overlay) - Create PLAN for DCT Phase A
UIS — New commands (independent, can start anytime)
- UIS maintainer creates
uis configurecommand (start with PostgreSQL) - UIS maintainer creates
uis exposecommand (port-forward + persistent NodePort) - UIS adds
configurable: true/falsefield toservices.json(needed for TMP validation) - UIS adds
exposePortfield toservices.jsonfor fixed port mappings - Create PLAN for UIS commands
DCT Phase B — Service integration (depends on: UIS commands ready)
- DCT maintainer creates Docker CLI install script
- DCT creates
uis-bridge.shlibrary (2MSG) - DCT creates
dev-template configurecommand - Support
requireshandling,params(interactive + CLI args per 3MSG) - Create PLAN for DCT Phase B
Future
-
uis templatecommand in UIS repo for infrastructure templates - TMP validation checks
requiresagainst UISservices.json(onlyconfigurableservices)