344 lines
13 KiB
Markdown
344 lines
13 KiB
Markdown
# Email Template Customization and Translation PRD
|
|||
|
|
|
||
|
|
## Purpose
|
||
|
|
Enable organization admins to manage outbound email templates without code changes by editing:
|
||
|
|
- Email logo
|
||
|
|
- Email content
|
||
|
|
- Whether each email should be sent
|
||
|
|
|
||
|
|
This also introduces localization so email content is not hardcoded to English only.
|
||
|
|
|
||
|
|
## Problem Statement
|
||
|
|
Today, all templates in `packages/email/src/emails/*.ts` are:
|
||
|
|
- Hardcoded in English
|
||
|
|
- Hardcoded in code (no admin editing surface)
|
||
|
|
- Always sent when the related business event occurs
|
||
|
|
- Coupled to a static ClassroomIO logo in `getDefaultTemplate(...)`
|
||
|
|
|
||
|
|
This limits localization, white-labeling, and product control.
|
||
|
|
|
||
|
|
## Decision Summary
|
||
|
|
- v1 allows editing system-managed email template IDs only.
|
||
|
|
- Org admins cannot create new email event types or custom triggers from the UI.
|
||
|
|
- Engineering can add new system email IDs where legacy raw emails still exist.
|
||
|
|
- v1 supports per-template:
|
||
|
|
- `logo`
|
||
|
|
- `content`
|
||
|
|
- `isEnabled` (send toggle)
|
||
|
|
- v1 supports locale variants with fallback behavior.
|
||
|
|
|
||
|
|
## Current-State Audit
|
||
|
|
|
||
|
|
Legend: `available now`, `partial`, `missing`
|
||
|
|
|
||
|
|
| Capability | Availability | Current State | Effort | Should Implement |
|
||
|
|
| --- | --- | --- | --- | --- |
|
||
|
|
| Edit logo per template | missing | Static logo URL in default wrapper | Medium | [x] |
|
||
|
|
| Edit content per template | missing | HTML strings are hardcoded in TS files | Medium | [x] |
|
||
|
|
| Per-email send toggle | missing | Email sends are trigger-based only | Medium | [x] |
|
||
|
|
| Multi-language email content | missing | English-only template copy | Medium | [x] |
|
||
|
|
| Create new email types | missing | Requires code updates in registry/constants | High | [ ] (v1 non-goal) |
|
||
|
|
| Template preview before save | missing | No preview API/UI | Low | [x] |
|
||
|
|
| Audit trail for template changes | missing | No history | Medium | [x] |
|
||
|
|
|
||
|
|
## Existing Email IDs (Current Catalog)
|
||
|
|
From `packages/email/src/utils/constants.ts`:
|
||
|
|
- `forgotPassword`
|
||
|
|
- `welcome`
|
||
|
|
- `verifyEmail`
|
||
|
|
- `onPasswordReset`
|
||
|
|
- `inviteTeacher`
|
||
|
|
- `newsfeedPost`
|
||
|
|
- `newsfeedComment`
|
||
|
|
- `studentCourseWelcome`
|
||
|
|
- `studentCourseInvite`
|
||
|
|
- `teacherStudentJoined`
|
||
|
|
- `teacherStudentBuyRequest`
|
||
|
|
- `studentProvePayment`
|
||
|
|
- `teacherCourseWelcome`
|
||
|
|
|
||
|
|
## Goals
|
||
|
|
1. Organization admins can configure logo, content, and send toggle for each existing email ID.
|
||
|
|
2. Admins can manage localized content using supported locales (`en`, `hi`, `fr`, `pt`, `de`, `vi`, `ru`, `es`, `pl`, `da`).
|
||
|
|
3. Send pipeline resolves the best template by organization + locale with safe fallback.
|
||
|
|
4. Template resolution should not break existing `sendEmail(...)` call sites.
|
||
|
|
5. Keep type-safe Zod field validation already provided by `@cio/email`.
|
||
|
|
6. Migrate raw `deliverEmail(...)` product notifications into managed template IDs.
|
||
|
|
|
||
|
|
## Non-Goals (v1)
|
||
|
|
- Letting admins create brand new email types, custom event triggers, or campaign flows.
|
||
|
|
- Editing `subject`, `from`, or `replyTo` in dashboard.
|
||
|
|
- Drag-and-drop email builder.
|
||
|
|
- Advanced analytics (open rate, click rate).
|
||
|
|
- Attachments.
|
||
|
|
|
||
|
|
## Legacy Raw Emails to Standardize
|
||
|
|
Current product emails bypassing template registry:
|
||
|
|
- `apps/api/src/services/submission/submission.ts`:
|
||
|
|
- student submission status update email
|
||
|
|
- teacher notification on new student submission
|
||
|
|
|
||
|
|
Plan:
|
||
|
|
- Create system-managed IDs for these flows.
|
||
|
|
- Move content to template registry + editable overrides.
|
||
|
|
- Keep behavior compatible while migrating call sites.
|
||
|
|
|
||
|
|
## Functional Requirements
|
||
|
|
|
||
|
|
### Template Editing
|
||
|
|
Per existing email ID, admins can:
|
||
|
|
- Upload/select logo URL
|
||
|
|
- Edit localized HTML content
|
||
|
|
- Enable or disable sending
|
||
|
|
|
||
|
|
### Localization
|
||
|
|
- Template content is saved per locale.
|
||
|
|
- Fallback order at send time:
|
||
|
|
1. Org + emailId + requested locale
|
||
|
|
2. Org + emailId + `en`
|
||
|
|
3. Code default template renderer (current hardcoded fallback)
|
||
|
|
|
||
|
|
### Send Toggle
|
||
|
|
- If template for an email ID is disabled, send is skipped and logged.
|
||
|
|
- Skip must not fail business actions (invites, onboarding, etc.).
|
||
|
|
- `required` email IDs can be protected from disabling (recommendation below).
|
||
|
|
|
||
|
|
### Placeholder Safety
|
||
|
|
- Content can use variable placeholders from each template schema (for example `{{inviteLink}}`, `{{studentName}}`).
|
||
|
|
- Save validation rejects unknown placeholders for the selected email ID.
|
||
|
|
- Render-time validation continues using existing Zod schema.
|
||
|
|
|
||
|
|
## Product Rules (Recommended)
|
||
|
|
1. Email IDs classified as:
|
||
|
|
- `required`: `forgotPassword`, `verifyEmail`, `onPasswordReset`
|
||
|
|
- `optional`: all remaining IDs
|
||
|
|
2. `required` IDs cannot be disabled in UI/API.
|
||
|
|
3. Missing localized content shows explicit fallback badge (`Using English fallback`).
|
||
|
|
|
||
|
|
## Proposed Architecture
|
||
|
|
|
||
|
|
### Data Model (New)
|
||
|
|
|
||
|
|
#### `organization_email_template`
|
||
|
|
Organization-scoped override rows for existing email IDs.
|
||
|
|
|
||
|
|
Suggested fields:
|
||
|
|
- `id` (uuid, pk)
|
||
|
|
- `organization_id` (uuid, fk -> `organization.id`, indexed)
|
||
|
|
- `email_id` (text, constrained to known email IDs)
|
||
|
|
- `locale` (`LOCALE`, default `en`)
|
||
|
|
- `is_enabled` (boolean, default `true`)
|
||
|
|
- `logo_url` (text, nullable)
|
||
|
|
- `content` (text, nullable; null means fallback to code default content)
|
||
|
|
- `updated_by_profile_id` (uuid, fk -> `profile.id`)
|
||
|
|
- `created_at`, `updated_at`
|
||
|
|
|
||
|
|
Constraints:
|
||
|
|
- unique `(organization_id, email_id, locale)`
|
||
|
|
- index `(organization_id, email_id)`
|
||
|
|
- index `(organization_id, locale)`
|
||
|
|
|
||
|
|
Notes:
|
||
|
|
- Row presence means override intent.
|
||
|
|
- `content = null` allows logo/toggle-only overrides.
|
||
|
|
|
||
|
|
#### `organization_email_template_audit`
|
||
|
|
Append-only audit trail of changes.
|
||
|
|
|
||
|
|
Suggested fields:
|
||
|
|
- `id` (uuid, pk)
|
||
|
|
- `organization_id` (uuid, indexed)
|
||
|
|
- `email_template_id` (uuid, fk -> `organization_email_template.id`)
|
||
|
|
- `action` (`CREATED` | `UPDATED` | `DELETED` | `ENABLED` | `DISABLED`)
|
||
|
|
- `before` (jsonb)
|
||
|
|
- `after` (jsonb)
|
||
|
|
- `actor_profile_id` (uuid)
|
||
|
|
- `created_at`
|
||
|
|
|
||
|
|
## Backend Plan
|
||
|
|
|
||
|
|
### Validation (`packages/utils/src/validation/email-template/`)
|
||
|
|
- `ZListEmailTemplatesQuery`
|
||
|
|
- `ZGetEmailTemplateParams`
|
||
|
|
- `ZUpsertEmailTemplate`
|
||
|
|
- `ZDeleteEmailTemplate`
|
||
|
|
- `ZPreviewEmailTemplate`
|
||
|
|
|
||
|
|
`ZUpsertEmailTemplate` (v1):
|
||
|
|
- `emailId: EmailId`
|
||
|
|
- `locale: TLocale`
|
||
|
|
- `isEnabled?: boolean`
|
||
|
|
- `logoUrl?: string | null`
|
||
|
|
- `content?: string | null`
|
||
|
|
|
||
|
|
### Queries (`packages/db/src/queries/email-template/`)
|
||
|
|
- `listOrganizationEmailTemplates`
|
||
|
|
- `getOrganizationEmailTemplate`
|
||
|
|
- `upsertOrganizationEmailTemplate`
|
||
|
|
- `deleteOrganizationEmailTemplate`
|
||
|
|
- `createOrganizationEmailTemplateAudit`
|
||
|
|
- `getResolvedOrganizationEmailTemplate` (with fallback)
|
||
|
|
|
||
|
|
### Services (`apps/api/src/services/email-template/`)
|
||
|
|
- `listEmailTemplatesService`
|
||
|
|
- `getEmailTemplateService`
|
||
|
|
- `upsertEmailTemplateService`
|
||
|
|
- `deleteEmailTemplateService`
|
||
|
|
- `previewEmailTemplateService`
|
||
|
|
- `resolveEmailTemplateForSendService`
|
||
|
|
|
||
|
|
### Routes (`apps/api/src/routes/email-template/`)
|
||
|
|
Mounted under organization context:
|
||
|
|
- `GET /organization/email-templates`
|
||
|
|
- `GET /organization/email-templates/:emailId`
|
||
|
|
- `PUT /organization/email-templates/:emailId`
|
||
|
|
- `DELETE /organization/email-templates/:emailId`
|
||
|
|
- `POST /organization/email-templates/:emailId/preview`
|
||
|
|
|
||
|
|
All routes: `authMiddleware` + `orgAdminMiddleware`.
|
||
|
|
|
||
|
|
## Send Pipeline Integration
|
||
|
|
|
||
|
|
### Current
|
||
|
|
`sendEmail(emailId, { to, fields, ... })` renders from registered code template directly.
|
||
|
|
|
||
|
|
### Proposed
|
||
|
|
Add optional send context:
|
||
|
|
- `organizationId?: string`
|
||
|
|
- `locale?: TLocale`
|
||
|
|
|
||
|
|
Flow:
|
||
|
|
1. Existing template schema validation remains unchanged.
|
||
|
|
2. If `organizationId` exists, resolve override with locale fallback.
|
||
|
|
3. Determine final `isEnabled`.
|
||
|
|
4. If disabled, return skipped result `{ success: true, skipped: true, reason: 'TEMPLATE_DISABLED' }`.
|
||
|
|
5. Render final email wrapper with resolved `logoUrl` and `content` (or default fallback).
|
||
|
|
|
||
|
|
Implementation note:
|
||
|
|
- Keep backward compatibility by making new context optional.
|
||
|
|
- Existing call sites without org context continue to use default code templates.
|
||
|
|
|
||
|
|
## Frontend Plan (Dashboard)
|
||
|
|
|
||
|
|
### Route
|
||
|
|
- `apps/dashboard/src/routes/org/[slug]/settings/emails/+page.svelte`
|
||
|
|
|
||
|
|
### Navigation
|
||
|
|
Add settings tab item under org settings:
|
||
|
|
- title: `Emails`
|
||
|
|
- path: `/settings/emails`
|
||
|
|
|
||
|
|
### Feature Module
|
||
|
|
`apps/dashboard/src/lib/features/email-template/`
|
||
|
|
- `api/email-template.svelte.ts`
|
||
|
|
- `utils/types.ts`
|
||
|
|
- `utils/email-template-utils.ts`
|
||
|
|
- `components/email-template-list.svelte`
|
||
|
|
- `components/email-template-editor.svelte`
|
||
|
|
- `components/email-template-preview.svelte`
|
||
|
|
- `pages/email-template-settings.svelte`
|
||
|
|
|
||
|
|
### UX (v1)
|
||
|
|
- Left panel: list of existing email IDs + enabled status.
|
||
|
|
- Top controls: locale selector + fallback indicator.
|
||
|
|
- Editor:
|
||
|
|
- logo upload/URL
|
||
|
|
- content editor (HTML)
|
||
|
|
- placeholder helper for selected email ID
|
||
|
|
- enable/disable switch
|
||
|
|
- Preview pane renders current locale result.
|
||
|
|
- Save + Reset actions.
|
||
|
|
|
||
|
|
### Copy and Translations
|
||
|
|
- No hardcoded UI strings.
|
||
|
|
- Add keys in `apps/dashboard/src/lib/utils/translations/en.json` and other locales as needed.
|
||
|
|
|
||
|
|
## Phased Rollout
|
||
|
|
|
||
|
|
### Phase 0: Foundation
|
||
|
|
1. DB tables + migration.
|
||
|
|
2. Validation + queries + services + routes.
|
||
|
|
3. Add metadata catalog for email IDs and allowed placeholders.
|
||
|
|
4. Add system-managed IDs for legacy raw submission emails.
|
||
|
|
|
||
|
|
### Phase 1: Dashboard Management
|
||
|
|
1. Add org settings email page.
|
||
|
|
2. List, edit, save, reset template overrides.
|
||
|
|
3. Preview endpoint + preview UI.
|
||
|
|
|
||
|
|
### Phase 2: Send Pipeline Adoption
|
||
|
|
1. Add optional `organizationId`/`locale` context in send flow.
|
||
|
|
2. Update org-scoped call sites to pass context.
|
||
|
|
3. Add skip logging for disabled templates.
|
||
|
|
|
||
|
|
### Phase 3: Hardening
|
||
|
|
1. Audit logs UI/read endpoint.
|
||
|
|
2. Placeholder validation improvements and warnings.
|
||
|
|
3. Performance optimizations and caching.
|
||
|
|
|
||
|
|
## Migration Plan
|
||
|
|
No hard data migration is required for v1 because defaults already exist in code.
|
||
|
|
|
||
|
|
Rollout strategy:
|
||
|
|
1. Ship schema and API with no behavioral change.
|
||
|
|
2. Ship UI and allow override creation.
|
||
|
|
3. Enable send-time override resolution.
|
||
|
|
4. Incrementally update send call sites to pass org/locale context.
|
||
|
|
|
||
|
|
## Risks and Mitigations
|
||
|
|
- Risk: Admin enters broken HTML.
|
||
|
|
- Mitigation: sanitize on save + render preview with warnings.
|
||
|
|
- Risk: Required variables removed from content.
|
||
|
|
- Mitigation: placeholder validation against schema keys.
|
||
|
|
- Risk: Disabling critical auth emails blocks account flows.
|
||
|
|
- Mitigation: `required` email IDs are non-disableable.
|
||
|
|
- Risk: Missing org context in some send call sites.
|
||
|
|
- Mitigation: safe fallback to existing code templates until upgraded.
|
||
|
|
- Risk: Large refactor footprint across services.
|
||
|
|
- Mitigation: optional send context and gradual adoption by domain.
|
||
|
|
|
||
|
|
## Success Metrics
|
||
|
|
- At least 80% of active organizations with non-default email templates in first 60 days.
|
||
|
|
- At least 90% of org-scoped email sends resolved through template override service.
|
||
|
|
- Zero critical auth incidents caused by template disable misconfiguration.
|
||
|
|
- Reduction in support requests about email branding/language by at least 50%.
|
||
|
|
|
||
|
|
## Open Questions
|
||
|
|
1. Should `welcome` email be org-customizable or remain global/system-owned?
|
||
|
|
2. Do we allow plain-text editor mode in addition to HTML mode?
|
||
|
|
3. Should logo override be per-template or org-global default with optional per-template override?
|
||
|
|
4. Should locale resolution use recipient profile locale automatically when context locale is omitted?
|
||
|
|
5. Do we need test-send endpoint in v1 or can preview-only ship first?
|
||
|
|
|
||
|
|
## Kanban Board (Ideation Phase)
|
||
|
|
|
||
|
|
| Ticket | Title | Todo | In Progress | Verification | Done |
|
||
|
|
| --- | --- | --- | --- | --- | --- |
|
||
|
|
| ET-1 | DB: add `organization_email_template` table | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-2 | DB: add `organization_email_template_audit` table | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-3 | Validation schemas for email-template APIs | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-4 | Queries for template CRUD + resolve fallback | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-5 | Service layer for list/get/upsert/delete/preview | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-6 | Organization routes `/email-templates` | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-7 | Dashboard settings page for templates | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-8 | Locale-aware editor and fallback indicators | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-9 | Preview rendering endpoint + UI pane | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-10 | Send pipeline context and override resolution | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-11 | Update org-scoped send call sites | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-12 | Required-email toggle guardrails | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-13 | Audit log write/read integration | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-14 | Caching/performance pass | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
| ET-15 | Migrate raw submission emails to managed template IDs | [ ] | [ ] | [ ] | [ ] |
|
||
|
|
|
||
|
|
## Verification Checklist
|
||
|
|
- [ ] `pnpm --filter @cio/db build`
|
||
|
|
- [ ] `pnpm --filter @cio/utils build`
|
||
|
|
- [ ] `pnpm --filter @cio/email build`
|
||
|
|
- [ ] `pnpm --filter @cio/api build`
|
||
|
|
- [ ] `pnpm --filter @cio/dashboard build`
|
||
|
|
- [ ] Manual: edit template logo + save + preview
|
||
|
|
- [ ] Manual: edit localized content + locale fallback behavior
|
||
|
|
- [ ] Manual: disable optional email and verify skip behavior
|
||
|
|
- [ ] Manual: ensure required emails cannot be disabled
|
||
|
|
- [ ] Manual: send flow still works for routes not yet migrated to org context
|