Template Catalog API
The template catalog manages the set of website and work scaffolding templates available to a user. It mixes built-in templates (seeded on module boot), repositories discovered via the user's GitHub installation, and arbitrary custom templates added by repository URL. Each user has a per-kind default that drives the new-work and new-website flows.
The catalog is rendered as a gallery in the platform dashboard's Templates section.
Architecture
apps/api/src/template-catalog/
template-catalog.controller.ts # 7 REST endpoints under /api/templates*
template-catalog.module.ts # Wires controller, agent module, activity-log
dto/list-templates.dto.ts # 7 DTOs (List/Add/Update/Archive/SetDefault/Fork/Refresh)
packages/agent/src/template-catalog/
template-catalog.service.ts # Seed, discovery, CRUD, fork, refresh
...
The agent-package TemplateCatalogService owns persistence and the GitHub
discovery pass (filtered to *template-suffix repositories under the user's
installations). The API controller is a thin layer that adds activity-log
emission on the five mutating endpoints.
Template kinds
Every endpoint is parameterised by kind:
| Kind | Purpose |
|---|---|
website | Templates for the public-facing website repository. |
work | Templates for the work content repository (work scaffolding). |
The kind is validated via class-validator's @IsIn(['website', 'work'])
on every DTO.
Built-in seeds
TemplateCatalogService.onModuleInit() upserts two built-in templates on
boot, wrapped in try/catch + warn-log so a seed failure never blocks app
startup:
| Template | Required env var | Notes |
|---|---|---|
Classic | always | The canonical default for new websites. |
Minimal | WEBSITE_TEMPLATE_MINIMAL_REPO | Optional; only seeded when the env var points at a <owner>/<repo> coordinate. |
Built-in templates are kept in sync across reboots via
findBuiltInByRepositoryCoordinates(owner, repo) so reseeding is idempotent.
REST endpoints
All endpoints sit behind the global AuthSessionGuard and resolve the user
exclusively from auth.userId. Five of the seven endpoints fire-and-forget
an activity-log row via .catch(() => {}) so an audit-write failure does
not break the user-facing endpoint.
GET /api/templates
List templates visible to the current user for one kind.
Query: ?kind=website or ?kind=work.
Response 200:
{
"status": "success",
"kind": "website",
"defaultTemplateId": "tmpl-classic",
"templates": [
{
"id": "tmpl-classic",
"name": "Classic",
"description": "...",
"framework": "Next.js",
"previewImageUrl": "https://...",
"repositoryUrl": "https://github.com/ever-works/website-classic-template",
"branch": "main",
"originType": "standard",
"kind": "website",
"isActive": true
}
]
}
The originType field discriminates display rules in the UI:
originType | Meaning |
|---|---|
standard | Built-in seed (Classic / Minimal). |
forked | A fork the user created from a built-in template via this catalog. |
custom_url | A custom template added via POST /api/templates/custom. |
POST /api/templates/custom
Add a custom template from a GitHub repository URL.
Request body:
{
"kind": "website",
"repositoryUrl": "https://github.com/owner/my-template",
"name": "My Template",
"description": "...",
"framework": "Next.js",
"previewImageUrl": "https://...",
"branch": "main",
"betaBranch": "beta"
}
Only kind + repositoryUrl are required (the URL is validated as http
or https). Defaults applied when fields are omitted:
branch→'main'.name→humanizeRepositoryName(repo)(e.g.my-template→My Template).framework→ inferred from the repository contents (inferFrameworkFromRepository).description/previewImageUrl/betaBranch→null.
Errors:
400 Bad Request—repositoryUrlis not a valid GitHub URL parsable byparseGitHubRepositoryUrl. Message:Only valid GitHub repository URLs are supported for custom templates.409 Conflict— duplicate. Message:You already added this template repository.
Activity log: template_added / template.added /
Added <kind> template: <name> with metadata: { templateId, kind }.
PUT /api/templates/custom/:templateId
Update a custom template's editable metadata. The owner is derived from
auth.userId; cross-user updates return 404.
The DTO follows three rules for each optional field:
undefined→ preserve the existing value.- empty-string → set to
null(clear) for nullable fields, otherwise preserve. - any other value → write it.
If branch changes, the service re-runs syncBranches so beta-vs-main
detection stays consistent.
Activity log: template_updated / template.updated.