Skip to content

Architecture: how the template system works

This page explains how starter kits are implemented — from the @protolabsai/templates package to the scaffold API endpoint to the UI template registry. Read this if you want to understand the internals or extend the system.

Overview

The template system has three layers:

UI template registry (apps/ui/src/lib/templates.ts)
        ↓  POST /api/setup/scaffold-starter
Server scaffold route (apps/server/src/routes/setup/routes/scaffold-starter.ts)
        ↓  imports
@protolabsai/templates package (libs/templates/)
        ↓  reads from
Starter source files (libs/templates/starters/<kit>/)

The UI layer defines what templates to show and how to present them. The server layer validates the request and delegates to the templates package. The templates package does the actual file I/O.

The @protolabsai/templates package

Location: libs/templates/

The package exports three scaffold functions and the types they use.

Types

StarterKitType — the union of valid kit names:

typescript
type StarterKitType = 'docs' | 'portfolio' | 'extension' | 'general';

ScaffoldOptions — input to every scaffold function:

typescript
interface ScaffoldOptions {
  /** Project name — used as the package.json name and in config substitution. */
  projectName: string;
  /** Absolute path to the destination directory (must not already exist). */
  outputDir: string;
}

ScaffoldResult — return value of every scaffold function:

typescript
interface ScaffoldResult {
  success: boolean;
  outputDir: string;
  filesCreated: string[];
  error?: string;
}

Scaffold functions

FunctionKit typeSource directory
scaffoldDocsStarter(options)docsstarters/docs/
scaffoldPortfolioStarter(options)portfoliostarters/portfolio/
scaffoldGeneralStarter(options)generalstarters/general/

All three functions follow the same pattern:

  1. Resolve the source directory path relative to the compiled package
  2. Recursively copy the directory to outputDir, skipping node_modules and package-lock.json
  3. Apply name substitutions to package.json and astro.config.mjs
  4. Return a ScaffoldResult with the list of top-level files created

The general starter additionally writes an app_spec.txt with the project name and a placeholder structure.

Name substitution

After copying, applySubstitutions() patches two files:

  • package.json — sets name to projectName
  • astro.config.mjs — updates the site URL, title, and description fields with projectName

If either file is missing (the general starter has no astro.config.mjs), the substitution is silently skipped.

The starters/ directory

Each starter kit is a self-contained project directory under libs/templates/starters/:

libs/templates/starters/
├── docs/
│   ├── .automaker/
│   │   └── CONTEXT.md
│   ├── .github/
│   │   └── workflows/
│   │       └── ci.yml
│   ├── src/
│   │   └── content/
│   │       └── docs/
│   ├── astro.config.mjs
│   └── package.json
├── portfolio/
│   ├── src/
│   │   ├── components/
│   │   ├── content/
│   │   └── pages/
│   ├── astro.config.mjs
│   └── package.json
└── general/
    └── .automaker/
        ├── settings.json
        └── categories.json

These files are the exact files that get copied to the user's project. Edit them directly to change what a scaffolded project looks like.

The scaffold server route

Location: apps/server/src/routes/setup/routes/scaffold-starter.ts

Endpoint: POST /api/setup/scaffold-starter

Request body:

FieldTypeRequiredDescription
projectPathstringYesAbsolute or relative path to the destination directory
kitType'docs' | 'portfolio' | 'general'YesWhich starter kit to scaffold
projectNamestringNoOverrides the name derived from the directory basename

Response:

typescript
{
  success: boolean;
  outputDir: string;      // resolved absolute path
  filesCreated: string[]; // top-level entries created
  error?: string;
}

The route:

  1. Validates projectPath and kitType
  2. Creates the target directory if it doesn't exist
  3. Resolves symlinks and checks against ALLOWED_ROOT_DIRECTORY (env var) to block path traversal
  4. Derives projectName from the directory basename if not provided
  5. Delegates to the matching scaffold function from @protolabsai/templates

Note: kitType: 'extension' is not accepted by this endpoint. The browser extension kit uses git clone and is handled separately.

The UI template registry

Location: apps/ui/src/lib/templates.ts

Defines the StarterTemplate interface and the starterTemplates array that the New Project dialog consumes.

typescript
interface StarterTemplate {
  id: string;
  name: string;
  description: string;
  source: 'scaffold' | 'clone';
  kitType?: 'docs' | 'portfolio' | 'extension'; // for scaffold source
  repoUrl?: string; // for clone source
  techStack: string[];
  features: string[];
  category: 'fullstack' | 'frontend' | 'backend' | 'ai' | 'other';
  author: string;
}

The source field determines how the UI provisions the project:

  • scaffold — sends POST /api/setup/scaffold-starter with kitType
  • clone — runs git clone repoUrl at the target path

Helper functions:

typescript
getTemplateById(id: string): StarterTemplate | undefined
getTemplatesByCategory(category: StarterTemplate['category']): StarterTemplate[]

Scaffold flow end-to-end

When a user creates a project from the New Project dialog:

1. User selects a template in new-project-modal.tsx
2. UI reads template.source
   ├── 'scaffold' → POST /api/setup/scaffold-starter { projectPath, kitType, projectName }
   │     └── Server calls scaffoldDocsStarter / scaffoldPortfolioStarter / scaffoldGeneralStarter
   │           └── copyDir(starters/<kit>/, outputDir) + applySubstitutions()
   └── 'clone'    → git clone <repoUrl> <projectPath>
3. On success, Studio opens the new project
FilePurpose
libs/templates/src/scaffold.tsScaffold functions and file I/O
libs/templates/src/types.tsStarterKitType, ScaffoldOptions, and related types
libs/templates/starters/Source files for each local scaffold kit
apps/server/src/routes/setup/routes/scaffold-starter.tsHTTP endpoint
apps/ui/src/lib/templates.tsUI template registry
apps/ui/src/components/dialogs/new-project-modal.tsxNew Project dialog

Built by protoLabs — Open source on GitHub