Design the workflow
A Skaff template is a directory with a TypeScript config file and a set of files to render. Keep the layout predictable so contributors can find everything quickly.
Directory layout
templates/
service-template/
templateConfig.ts
templates/
package.json.hbs
src/index.ts.hbs
subtemplates/
auth/
templateConfig.ts
templates/
templateConfig.tsexports aTemplateConfigModule. Define metadata (name,description,specVersion), the Zod schemas for user input (templateSettingsSchemaandtemplateFinalSettingsSchema), and themapFinalSettingsfunction that produces the values used during rendering. SetisRootTemplatetotruefor templates that can start a project and configuretargetPathso Skaff knows where files land.templates/holds files to render. Append.hbsto process a file with Handlebars; omit it for plain copies. Skaff copies the directory structure relative totargetPath.subtemplates/hosts optional modules. Structure them the same way as root templates so they can run independently.
Model user input
Use Zod to describe every prompt. Each field becomes a CLI question and a form control in the web app. Provide helpful descriptions so teams understand how values are used.
templateConfig.ts
import z from "zod";
import type {
FinalTemplateSettings,
TemplateConfigModule,
} from "@timonteutelink/template-types-lib";
const templateSettingsSchema = z.object({
projectRepositoryName: z
.string()
.min(1)
.describe("NPM package name"),
includeAuth: z
.boolean()
.default(false)
.describe("Generate auth routes and providers"),
});
type TemplateSettings = z.infer<typeof templateSettingsSchema>;
const templateConfig: TemplateConfigModule<
FinalTemplateSettings,
typeof templateSettingsSchema,
typeof templateSettingsSchema
> = {
templateConfig: {
name: "service-template",
author: "Example team",
description: "Production-ready service starter",
specVersion: "1.0.0",
isRootTemplate: true,
},
templateSettingsSchema,
templateFinalSettingsSchema: templateSettingsSchema,
mapFinalSettings: ({ templateSettings }): TemplateSettings =>
templateSettings,
targetPath: ".",
};
export default templateConfig;
Adjust mapFinalSettings whenever you need to derive additional values—such as generated slugs, computed booleans, or data pulled from the parent settings.
Wire up subtemplates
Subtemplates keep optional features separate:
- Use
autoInstantiatedSubtemplatesin the parent to include children automatically based on settings. - Mark subtemplates as
multiInstancewhen they can be reused (for example, to add routes or jobs repeatedly). Make sure file paths include a unique identifier to avoid collisions. - Share types between parents and children so settings stay in sync.
Document available subtemplates in the template repository README so teams know which features they can enable.