Skip to content

Shank Email Templates

Scope

Shank is not a runtime service. It is a build-time React Email project that compiles JSX templates to static HTML and writes the result into sirloin’s embedded template directory. There is no network surface, no endpoints, no public API.

The “API” of shank is the set of HTML files it produces and the placeholder tokens those files contain. Sirloin’s internal/pkg/emails package treats these as plain string templates and substitutes {{TOKEN}} placeholders at send time (see apps/sirloin/internal/pkg/emails/client.go).

Source location

  • React Email project: apps/shank/react-templates/
  • Templates: apps/shank/react-templates/emails/*.tsx
  • Export target (committed): apps/sirloin/internal/pkg/emails/templates/*.html

The export path is hardcoded in apps/shank/react-templates/package.json:

"export": "email export --outDir='../../sirloin/internal/pkg/emails/templates'"

Exported templates

Source TSXExported HTMLSirloin sender
emails/training-done.tsxtemplates/training-done.htmlClient.SendTrainingDone
emails/training-failed.tsxtemplates/training-failed.htmlnone — client.go loads the template into c.templateTrainingFailed but no SendTrainingFailed method exists in apps/sirloin/internal/pkg/emails/client.go (verified by grep -rn 'SendTrainingFailed' apps/sirloin, no matches). The template is currently dead code on the sirloin side.

training-done

  • Subject (set by sirloin): "Training completed for " + characterName + "!"
  • Preview text: Your character {{CHARACTER_NAME}} is ready on Foxy AI!
  • Placeholders:
    • {{CHARACTER_NAME}} — display name of the trained character
    • {{CHARACTER_ID}} — UUID, used in deep-link https://app.foxy.ai/?trainingSuccessId={{CHARACTER_ID}}
  • Sirloin call site:
apps/sirloin/internal/pkg/emails/client.go
func (c *Client) SendTrainingDone(
ctx context.Context,
userID, characterName string,
characterID uuid.UUID,
) error

Caller: apps/sirloin/internal/app/worker/checkmediageneration.go:262.

training-failed

  • Preview text: Your character {{CHARACTER_NAME}} training failed
  • Placeholders: {{CHARACTER_NAME}} only (verified by grep -oE '\{\{[A-Z_]+\}\}' apps/sirloin/internal/pkg/emails/templates/training-failed.html). The source TSX exports TrainingDoneEmail as its default — a copy-paste artifact tracked in shank-errors.md; the exported HTML itself does not surface the wrong component name beyond the preview text.
  • Sirloin sender: none — apps/sirloin/internal/pkg/emails/client.go loads the template into c.templateTrainingFailed but no SendTrainingFailed method exists (grep -rn 'SendTrainingFailed' apps/sirloin returns no matches). The template is currently dead code on the sirloin side.

How sirloin consumes the templates

apps/sirloin/internal/pkg/emails/client.go embeds the template directory at compile time:

//go:embed templates
var emailTemplates embed.FS

On NewClient, it reads training-done.html and training-failed.html into in-memory strings. Each Send* method does straight strings.ReplaceAll substitution on the {{TOKEN}} placeholders, then sends via SMTP+TLS using credentials passed to NewClient.

This means:

  • Templates are part of the sirloin binary after pnpm export + go build.
  • Shank changes are not deployed independently — sirloin must be rebuilt and redeployed to pick them up.
  • New placeholders require two coordinated changes: the template (shank) and the substitution call (sirloin).

Adding a new template

  1. Add apps/shank/react-templates/emails/<name>.tsx.
  2. Use {{TOKEN}} placeholders for any dynamic content. Tokens are uppercase snake-case by convention ({{CHARACTER_NAME}}, {{CHARACTER_ID}}).
  3. Run pnpm export from apps/shank/react-templates/.
  4. Add a loader branch in Client.NewClient for the new HTML file.
  5. Add a Send<Name> method that does the strings.ReplaceAll substitution and calls c.send.
  6. Wire the caller (typically a worker in apps/sirloin/internal/app/worker/).
  7. Commit both the source TSX and the exported HTML.

See shank-runbook.md for the full release flow.