Prima Get an API key

Module kit / Messaging, email and SMS

Workspace Email Branding Shell

messaging-email-sms/email-branding

Kind backend
Domain messaging-email-sms
Maturity prod
Reusable extract
Stack fastapi-0.118, sqlalchemy-2.0-sync, postgres, pydantic-2.12
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/workspace-scoping, _shared/error-envelope
Frontends messaging-email-sms/ui/email-template-editor
Reference implementation
  • alchemy_labs/backend/services/email_design.py
  • alchemy_labs/backend/api/routers/workspace_router.py
Measured on alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12
Version 1

Purpose

A per-workspace email_design config (logo, brand color, footer HTML, reusable {{block:NAME}} snippets), stored in the existing workspace_settings.settings column, lets any HTML body a workspace sends (a service-desk requester reply, a form or product message step) render inside that workspace's own 600px branded card instead of Alchemy's own template, with a clean unbranded shell when nothing is configured. The invariant that makes it trustworthy is the one render_branded_email's own docstring states and enforces with two nested try/except blocks (email_design.py 210 to 211, 214 to 218, 243 to 255): it never raises, and - since commit 1b0c0ec ("email: a client's mail no longer borrows Alchemy's logo") - an unconfigured workspace's mail carries no Alchemy branding at all rather than silently defaulting to Alchemy's own logo and footer (email_design.py 220 to 241).

When to use

Trigger phrases: "email design", "email_design", "branded email", "brand shell", "email-design", {{block:, "from_name", "logo_url", "brand_color", "footer_html", "unbranded fallback". Read it before touching _brand_shell, render_branded_email, _substitute, get_email_design/save_email_design/delete_email_design, or the three /email-design routes; and when someone asks "why does this client's mail still say Alchemy Zürich", "why did {{block:x}} show up literally in an email", or "why does a workspace's branding disappear on the fallback path".

Not this recipe: the transport funnel itself, email_service._send_email/_get_base_template/send_via_smtp (services/email_service.py), which is what render_branded_email falls back to on error and what every caller ultimately calls to actually send; the desk mailbox override that can replace that funnel for service-desk mail, services/service_desk/inbox_config.send_via_mailbox (notifications.py 71 to 77); the scheduling/dispatch of which body gets rendered and when - messaging_service.send_step's templating, {{var}} sourcing, .ics attachment and step timing (messaging_service.py 266 to 340 and around), and the service-desk notification event catalogue (service_desk/notifications.py's five requester events) - both are callers of this recipe's one function, not this recipe; the workspace's own outbound SMTP relay credentials, services/smtp_config (a separate credential-holding recipe messaging_service.py imports at line 312 to decide transport, after this recipe has already rendered the HTML).

Stack assumptions

At 62ba4fc: fastapi==0.118.2, synchronous sqlalchemy.orm.Session (SQLAlchemy==2.0.43), psycopg2-binary==2.9.10, pydantic==2.12.0 for the PUT body (EmailDesignPut, workspace_router.py 1056 to 1063). No PSP, HTTP client or scheduler library is imported by email_design.py itself - it is pure SQLAlchemy text() plus re/html/json, called synchronously inside async route handlers and inside two other services' own send paths. Foundations assumed: S4 (fastapi-project-layout) for mount_router and the app that mounts workspace_router.router (app_fastapi.py 828); S5 (auth-dependency-pattern) for get_current_user (api/dependencies.py 187 to 195, JWT Bearer > httpOnly cookie > X-API-Key, in that priority); S6 (workspace-scoping) for _assert_ws_access/IsolationService.verify_workspace_access (workspace_router.py 996 to 999; services/workspace/isolation.py 109 to 135); S7 (error-envelope) for the shape the 403 from _assert_ws_access and FastAPI/Pydantic's own 422s come back as.

No dedicated environment variable exists for this recipe (grep -n "email_design\|EMAIL_DESIGN" core/config.py finds nothing) - the module's own docstring states it deliberately: no migration needed, nothing sensitive to protect (email_design.py 4). Fail behaviour is the module's central guarantee: render_branded_email wraps the get_email_design read in try/except (214 to 218, falls back to design = None) and wraps the substitute-plus-wrap step in a second try/except (243 to 255, falls back to email_service._get_base_template(body_html) with no brand argument - see Gotchas for what that specific fallback actually renders); the module docstring's own words are "Robust: any error falls back to _get_base_template, never raises" (210 to 211).

Data model

No migration. workspace_design lives inside the existing workspace_settings table's settings column, one JSON key among sibling keys (payrexx_config, quentn_config, and others), never a table of its own:

-- backend/migrations/versions/baseline_prod_schema_001.py 2231-2238 (unchanged since baseline)
CREATE TABLE public.workspace_settings (
    id uuid NOT NULL,
    workspace_id uuid NOT NULL,
    settings json NOT NULL,             -- note: "json", not "jsonb" - see below
    created_at timestamp without time zone,
    updated_at timestamp without time zone
);
-- pk_workspace_settings PRIMARY KEY (id); uq_workspace_settings_workspace_id UNIQUE (workspace_id)
-- idx_workspace_settings ON workspace_id (baseline_prod_schema_001.py 2882-2883, 3061-3063, 3643)

Measured discrepancy, worth flagging plainly: email_design.py's own module docstring calls this column "JSONB" (line 4), and so does this recipe's own assignment brief. The actual Postgres column, per the baseline migration above, is json, not jsonb; every write in email_design.py casts to that same type (CAST(:s AS json), lines 94, 101 and 115), consistent with the schema, not with the docstring's parenthetical. A rebuild that types the column jsonb to match the docstring changes on-disk representation (key order, whitespace) and should treat that as a deliberate deviation, not a typo fix, since nothing here depends on jsonb's operators.

The email_design key's shape, normalized on read by get_email_design (email_design.py 54 to 67) and written field-by-field by save_email_design (70 to 106, only supplied keys are touched, letting the API patch one field at a time):

// workspace_settings.settings.email_design
{
  "from_name": "string | null",          // display name on outbound mail; read by callers, not used inside this file
  "logo_url": "string | null",           // rendered as <img> in the header; empty renders "&nbsp;" instead
  "brand_color": "string | null",        // header background; defaults to "#6844C4" when unset
  "accent_text_color": "string | null",  // header text/icon color; defaults to "#FFFFFF" when unset
  "footer_html": "string | null",        // raw HTML, dropped into a bordered footer block verbatim if truthy
  "blocks": { "<name>": "<html>" }       // reusable snippets, referenced from a body via {{block:name}}; save_email_design stores every key/value coerced to str (85)
}

delete_email_design (109 to 118) only pops the email_design key from the loaded settings dict and writes the rest back; it is a no-op, not an error, when the row or the key does not exist (111 to 112).

What is NOT stored, and not enforced by this file: no size cap on footer_html or any blocks value (EmailDesignPut in workspace_router.py 1056 to 1063 declares every field a bare Optional[str]/Optional[Dict[str, str]], no max_length, no HTML sanitizer call anywhere in email_design.py or the router); no audit trail of who set or changed a workspace's branding (no updated_by column, workspace_settings itself has no such column either); no separate table, this recipe adds zero rows anywhere, it only rewrites one JSON key on a row that already exists for other settings.

API surface

RouteAuthTenancyBodyCodesNotes
GET /api/workspaces/{workspace_id}/email-design (workspace_router.py 1066 to 1075, prefix from router = APIRouter(prefix="/api/workspaces", ...) at line 29)Depends(get_current_user)_assert_ws_access (996 to 999)none200 EmailDesignConfig shape above, or {"configured": false} when unset; 403 no workspace access; 422 malformed workspace_id (FastAPI's own UUID path validation)Never 404 - an unconfigured workspace still answers 200
PUT /api/workspaces/{workspace_id}/email-design (1078 to 1089)Depends(get_current_user)_assert_ws_accessEmailDesignPut (1056 to 1063): from_name, logo_url, brand_color, accent_text_color, footer_html: Optional[str] = None, blocks: Optional[Dict[str, str]] = None200 the saved config (same shape as GET); 403; 422 on a non-dict blocks value per Pydanticbody.dict(exclude_none=True) (1089) - an omitted field is left untouched by save_email_design, not cleared; clearing a field means sending it explicitly (an empty string, coerced to None at email_design.py 81)
DELETE /api/workspaces/{workspace_id}/email-design (1092 to 1101)Depends(get_current_user)_assert_ws_accessnone200 {"configured": false} always, whether or not a design existed; 403Removes the whole email_design key, there is no per-field DELETE

The one function this recipe actually exists to document is not a route at all, it is called directly from two other services' own send paths:

FunctionRaisesReturns
render_branded_email(db, workspace_id, body_html, variables=None) (email_design.py 196 to 256)never (module docstring 210 to 211)the full <!DOCTYPE html>…</html> string to hand to the mail transport

Application logic

render_branded_email, in order (196 to 256):

  1. Normalize inputs: body_html = body_html or "", variables = variables or {} (212 to 213).
  2. Load the workspace's design via get_email_design, inside a try/except that logs a warning and sets design = None on any failure (214 to 218) - a broken read degrades to the unbranded path, it does not propagate.
  3. No design (get_email_design returned None or an empty dict, get_email_design itself returns None for a non-dict or empty ed, 58 to 59): render with _substitute(body_html, {}, variables) - an empty blocks map, so every {{block:NAME}} token is dropped, never leaked (_substitute 128) - then _brand_shell(...) called with every branding kwarg None (235 to 241). The comment at 220 to 234 names exactly why this branch exists and what it replaced: before this fix, an unconfigured workspace's mail fell back to email_service._get_base_template, which hardcodes Alchemy Zürich's own logo, wordmark and a named Alchemy address in the footer - correct for platform mail, wrong for a client's own customers. _brand_shell already renders no <img> when logo_url is falsy (156 to 162, header_inner = "&nbsp;"), so this branch is a clean, brandless card, not Alchemy's.
  4. Design present: _substitute(body_html, design.get("blocks") or {}, variables) then _brand_shell(...) with the workspace's own logo_url/brand_color/accent_text_color/footer_html (243 to 251).
  5. Anything raised inside step 4 is caught and answered with email_service._get_base_template(body_html) (252 to 255) - not _substitute's output, the raw, unrendered body_html (any {{block:NAME}}/{{var}} tokens reach the recipient literally on this one path) - see Gotchas for the branding this specific fallback carries.

_substitute (121 to 141), always in this order: (a) {{block:NAME}} (regex _BLOCK_RE, 33) replaced with blocks.get(name), an unknown name replaced with "", dropped rather than left as literal template syntax (124 to 130); (b) {{var}} (regex _VAR_RE, 35) replaced only for keys present in the variables dict, HTML-escaped via html.escape (132 to 140); a key not present in variables is left untouched verbatim (138, m.group(0)). Boundary worth calling out: _substitute itself does not escape body_html against a recipient's own typed text that happens to contain {{block:...}} - the caller service_desk/notifications._esc (23 to 33) pre-escapes agent/requester free text ({{ → &#123;&#123;) before it ever reaches render_branded_email. A new caller that skips that pre-escaping step, and writes user-typed text directly into body_html, lets that user trigger block expansion.

_brand_shell (144 to 193): fixed 600px card (max-width: 600px, 183); bg = brand_color or "#6844C4" (153), fg = accent_text_color or "#FFFFFF" (154); header renders <img src="{logo_url}" ...> only when logo_url is truthy, else &nbsp; (156 to 162); footer block rendered only when footer_html is truthy, wrapped in a bordered <div> (164 to 171); forces light mode via meta[name=color-scheme]/meta[name=supported-color-schemes] and an inline color-scheme: light on <body> (178 to 179, 182) - added by commit fbe8930 ("force light color-scheme in the branded email shell") so a dark-mode mail client cannot invert the workspace's own brand colors.

Invariants

  • render_branded_email never raises, on any input (module docstring 210 to 211; enforced by the two try/except blocks at 214 to 218 and 243 to 255).
  • An unknown {{block:NAME}} token is dropped, never rendered back as literal {{block:...}} syntax and never left to match a later, different block by accident (_substitute 124 to 130; test_template_tokens_in_ticket_text_are_never_expanded, see Tests).
  • An unconfigured workspace's mail carries none of Alchemy's own branding (fixed 2026, commit 1b0c0ec; _brand_shell(logo_url=None, brand_color=None, accent_text_color=None, footer_html=None) on the no-design path, 235 to 241) - except on the error-fallback path at step 5 above, which is Alchemy-branded regardless of workspace (see Gotchas).
  • save_email_design never clobbers a sibling settings key: it is a read-merge-write over the whole settings dict, the same pattern the module docstring names for payrexx_config/quentn_config (18 to 19; _load_settings 38 to 51, the merge at 70 to 89).
  • Any workspace member, at any role, may read, write or delete the workspace's own branding - _assert_ws_access calls verify_workspace_access with required_role=None (workspace_router.py 997 to 999; isolation.py 109 to 135 accepts any active WorkspaceUser row when no role is passed). There is no owner/admin-only gate on this specific surface.

Frontend

messaging-email-sms/ui/email-template-editor   REQUIRED by this recipe's scope; does NOT exist as a
                                                 built frontend module at 62ba4fc. `grep -rl
                                                 "email-design|emailDesign|email_design" frontend/src`
                                                 returns nothing - there is no settings page, form or
                                                 preview for a workspace to set from_name/logo_url/
                                                 brand_color/footer_html/blocks. The nearest existing
                                                 pattern is the raw GET/PUT/DELETE JSON endpoints above,
                                                 called by nothing in this repo's own frontend; a
                                                 workspace's branding can currently only be set by a
                                                 direct API call. This is a genuine gap in the reference
                                                 implementation, not a detail this recipe omitted.

Minimum admin/debug surface: none dedicated exists. The closest thing to inspection is GET /api/workspaces/{workspace_id}/email-design itself (returns {"configured": false} for an unset workspace) and, by hand, SELECT settings->'email_design' FROM workspace_settings WHERE workspace_id = :w.

Security & tenancy

Auth dependency: get_current_user (api/dependencies.py 187 to 195), JWT Bearer, then httpOnly cookie, then X-API-Key, in that priority. Tenancy: _assert_ws_access(db, current_user, workspace_id) on all three routes (workspace_router.py 996 to 999) calls IsolationService.verify_workspace_access(current_user.id, workspace_id) with required_role left at its default None (109 to 135) - any active membership row (WorkspaceUser.is_active == True) is sufficient at every one of owner/admin/member/viewer; there is no stronger role check anywhere in this recipe's three routes, so a viewer-role member can overwrite the whole workspace's outbound-mail branding.

Fail direction: _assert_ws_access fails closed, 403 on no membership row (996 to 999); render_branded_email fails toward not leaking Alchemy's own identity on the ordinary unconfigured path (step 3 above) but fails toward Alchemy's own branded template on the internal-error path (step 5, _get_base_template with no brand kwarg - see Gotchas), an intentional asymmetry worth knowing rather than assuming both failure paths behave the same.

Secrets and PII at rest: none owned by this recipe - email_design holds no credential, from_name/logo_url/footer_html/blocks are all workspace-authored display content the workspace already intends to be public in its own outbound mail. logo_url is stored and rendered as a plain <img src> with no fetch, allowlist or content check on the URL (156 to 162) - an attacker with write access to a workspace's own settings (i.e. any member of that workspace) can point it at any URL; this is a self-service branding field for the workspace's own tenant, not a cross-tenant concern, but a caller embedding render_branded_email's output somewhere an external actor's URL could land (not the case at 62ba4fc) would inherit that.

Rate limits: none dedicated to these three routes beyond whatever LABS_SURFACES and the app's own global middleware apply (app_fastapi.py 828, mount_router(..., surfaces=LABS_SURFACES)). Attacker-controlled inputs, all behind a valid session and workspace membership: every EmailDesignPut field is an unbounded Optional[str] with no max_length and blocks an unbounded Dict[str, str] (workspace_router.py 1056 to 1063) - a member can store arbitrarily large HTML in footer_html or any block value; nothing in this recipe caps it.

Tests

One real test exercises this recipe end to end, through the service-desk mail path (not through the HTTP /email-design routes directly): test_template_tokens_in_ticket_text_are_never_expanded (backend/tests/test_desk_lifecycle.py 912 to 933), using the desk fixture (test_desk_lifecycle.py 43 to 65, hermetic Postgres via db_session, frozen clock) and the mail_log fixture that monkeypatches email_service._send_email to a recorder (tests/desk_support.py 179 to 188, "nothing leaves the box"). It calls save_email_design directly (not the PUT route) with a blocks: {"signature": "<b>GEHEIMER BLOCK</b>"} config, then asserts a ticket title/body containing {{block:signature}} and {{ block:signature }} never leaks GEHEIMER BLOCK into the sent mail, that the literal token itself survives escaped (&#123;&#123;), and that an unknown block name ({{block:x}}) in agent-typed text is likewise never expanded and never silently dropped from the visible text.

Run 2026-09-12 against the postgres-test Docker service on :5434 (docker-compose.test.yml, the same one tests/conftest.py defaults TEST_DATABASE_URL to, 44 to 47), from alchemy_labs/backend using the .venv at backend/.venv:

WEBAUTHN_RP_ID_BACKFILL=localhost .venv/bin/python -m pytest \
    tests/test_desk_lifecycle.py -k test_template_tokens_in_ticket_text_are_never_expanded \
    -q --no-cov -o addopts="" -p no:cacheprovider

prints 1 passed, 40 deselected, 206 warnings in 7.52s (warnings are unrelated Pydantic v2/datetime.utcnow deprecations elsewhere in the suite, not this recipe).

Hermetic vs live: fully hermetic, no PSP, no real SMTP/Resend call, mail_log intercepts the one transport seam both messaging_service.send_step and service_desk/notifications.send_requester_mail ultimately call.

What a rebuild adds and this branch lacks, stated plainly: no dedicated unit test file for email_design.py itself - find . -iname "*email_design*" under alchemy_labs turns up only the module, its __pycache__, and a stale local htmlcov coverage artifact (backend/htmlcov/z_de3833460954761d_email_design_py.html, dated 2026-09-11, reporting 43% line coverage for this file - a leftover local report, not regenerated for this recipe, read as a rough signal only, not a measured-today number); no test calls the GET/PUT/DELETE /email-design HTTP routes directly - the only coverage of this recipe's persistence functions is the one direct Python call to save_email_design above, get_email_design/delete_email_design and the three routes' own auth/tenancy wiring are exercised by nothing found in this repo's test suite; no test exercises the error-fallback branch at email_design.py 243 to 255 (the _get_base_template path) at all.

Deploy & ops

No dedicated config key, no migration (Data model). Deploy is the app's own: workspace_router.router is mounted with surfaces=LABS_SURFACES (app_fastapi.py 828), which this recipe inherits without adding anything. HEAD of the main branch measured for this recipe (62ba4fc41009b454e1c033e5036c7e8577db2061) is identical to origin/main (git rev-parse HEAD origin/main prints the same hash twice, verified 2026-09-12, git status --short --branch shows main...origin/main with no ahead/behind) - this is the deployed trunk, not a feature branch, which together with the history below is the evidence for maturity: prod.

History on this file, in order: ce406e8 (2026-06-18, "feat(forms): unified messaging + per-workspace email design + grouped settings nav") introduced email_design.py and the three routes in the same commit as the wider messaging-steps feature; fbe8930 ("fix(email): force light color-scheme in the branded email shell") added the color-scheme meta tags and body style (178 to 179, 182); 1b0c0ec ("email: a client's mail no longer borrows Alchemy's logo") is the most recent commit touching this file and added the no-design-is-unbranded behaviour (220 to 241) this recipe's Purpose invariant depends on. Two live production callers exist at 62ba4fc: messaging_service.send_step (288 to 291, 317 to 320) for form/product message steps, and service_desk/notifications.send_requester_mail (63 to 77) for the five requester-facing desk events - the service desk itself shipped "live on prod" per its own commit dd38691.

Argus-style check: GET /api/workspaces/{workspace_id}/email-design for any workspace a caller belongs to answers 200 with either the saved config or {"configured": false}, never 500 - a 500 here means _load_settings's raw-SQL SELECT (38 to 42) is broken, not that a workspace is merely unconfigured. Rollback: nothing to unwind beyond the code itself - email_design is one optional key inside an existing row; reverting the code leaves any already-saved branding inert (ignored by the reverted render_branded_email) but not corrupted, and a re-deploy picks it straight back up.

Gotchas

  • 2026-09-12 (measured at 62ba4fc, email_design.py 4 vs backend/migrations/versions/baseline_prod_schema_001.py 2231 to 2238): the module docstring calls the storage column "JSONB"; the actual Postgres column, and every CAST(:s AS json) in this file's own writes (94, 101, 115), is json, not jsonb. Not a bug, the code and the schema agree with each other, only the docstring's parenthetical is wrong. A rebuild that assumes jsonb semantics (key ordering, operator support) is assuming something this table does not have.
  • 2026-09-12 (measured at 62ba4fc, email_design.py 243 to 255): the internal-error fallback calls email_service._get_base_template(body_html) with no brand argument, which - per _get_base_template's own docstring (email_service.py 1907 to 1919) - renders the Alchemy Zürich footer and header (name, street address) unless brand="prima" is passed. So the one failure path inside render_branded_email that is reachable by any rendering exception (a malformed blocks value, for instance) is the one path that puts Alchemy's own identity on a workspace's mail, the exact leak commit 1b0c0ec otherwise closed on the ordinary no-design path. Also worth noting: this fallback renders raw, un-substituted body_html - any {{block:...}}/{{var}} token in it reaches the recipient literally, since _substitute is what raised.
  • 2026-09-12 (measured at 62ba4fc, workspace_router.py 997 to 999): no route in this recipe passes a required_role to verify_workspace_access, so a viewer-role member can PUT or DELETE the whole workspace's outbound-mail branding, same as an owner. Whether that is the intended bar is not written down anywhere in this file or its tests.
  • 2026-09-12 (measured at 62ba4fc): no frontend surface exists for this recipe at all (Frontend section) - every workspace that has email_design configured today got there by a direct API call, not through any UI in this repo.

Reference implementation

Paths relative to the repo root; both files at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.

  • alchemy_labs/backend/services/email_design.py (255 lines, whole file in scope): module docstring and config shape (1 to 20); _load_settings (38 to 51); get_email_design (54 to 67); save_email_design (70 to 106); delete_email_design (109 to 118); block/var regexes _BLOCK_RE/_VAR_RE (33, 35); _substitute (121 to 141); _brand_shell (144 to 193); render_branded_email (196 to 256).
  • alchemy_labs/backend/api/routers/workspace_router.py: router prefix /api/workspaces (29); _assert_ws_access (996 to 999); EmailDesignPut (1056 to 1063); GET .../email-design (1066 to 1075); PUT .../email-design (1078 to 1089); DELETE .../email-design (1092 to 1101).
  • Callers (context, not this recipe's own scope): alchemy_labs/backend/services/messaging_service.py send_step (266 to roughly 340, render_branded_email call at 288 to 289, get_email_design call at 317 to 318); alchemy_labs/backend/services/service_desk/notifications.py _esc (23 to 33), send_requester_mail (58 to 83).
  • Schema: alchemy_labs/backend/migrations/versions/baseline_prod_schema_001.py 2229 to 2238 (workspace_settings table), 2882 to 2883 and 3061 to 3063 (primary key, unique constraint on workspace_id), 3641 to 3643 (index).
  • Fallback template compared against: alchemy_labs/backend/services/email_service.py _get_base_template (1907 to roughly 1930, the Alchemy Zürich vs brand="prima" branch).
  • Auth/tenancy foundations: alchemy_labs/backend/api/dependencies.py get_current_user (187 to 195); alchemy_labs/backend/services/workspace/isolation.py verify_workspace_access (109 to 135).
  • Mount: alchemy_labs/backend/app_fastapi.py 828 (mount_router(app, workspace_router.router, ..., surfaces=LABS_SURFACES)).
  • Test: alchemy_labs/backend/tests/test_desk_lifecycle.py test_template_tokens_in_ticket_text_are_never_expanded (912 to 933), fixtures desk (43 to 65) and imports from tests/desk_support.py (mail_log 179 to 188).

Deviations of the reference from an ideal implementation, all dated 2026-09-12: no frontend exists for configuring email_design at all (Frontend); the docstring's "JSONB" claim does not match the actual json column (Gotchas); the internal-error fallback re-introduces Alchemy branding and un-substituted tokens on the one path meant to be a safety net (Gotchas); no field-level size cap on footer_html/blocks values (Security & tenancy); no role gate stronger than plain membership on write/delete (Security & tenancy); no dedicated test file or HTTP-level test for this recipe's own three routes (Tests).

Acceptance checks

Run from alchemy_labs/backend at 62ba4fc (or an app built to this recipe) with the postgres-test Docker service reachable on :5434.

  1. WEBAUTHN_RP_ID_BACKFILL=localhost .venv/bin/python -m pytest tests/test_desk_lifecycle.py -k test_template_tokens_in_ticket_text_are_never_expanded -q --no-cov -o addopts="" -p no:cacheprovider prints 1 passed, 40 deselected, ... warnings in ...s (measured 2026-09-12: 7.52s).
  2. grep -n "settings json NOT NULL" backend/migrations/versions/baseline_prod_schema_001.py prints one match at line 2233, confirming the column is json, not jsonb, against the module docstring's claim.
  3. grep -c "CAST(:s AS json)" backend/services/email_design.py prints 3 (the two writes inside save_email_design plus the one inside delete_email_design), consistent with check 2.
  4. grep -n "^def render_branded_email" backend/services/email_design.py prints line 196; grep -n "from services.email_design import render_branded_email" backend/services/messaging_service.py backend/services/service_desk/notifications.py prints one match in each file, confirming both production callers import the shared function rather than reimplementing it.
  5. git rev-parse HEAD origin/main (from alchemy_labs, with origin fetched) prints the same hash twice - confirms maturity: prod rests on this being the deployed trunk, not a diverged branch.
  6. grep -rl "email-design\|emailDesign\|email_design" frontend/src (from alchemy_labs) returns no output, confirming the Frontend section's "does not exist" claim rather than assuming it.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.