Messaging (Ruellia)
Frond sends all transactional messaging (canvas-share emails, publish notifications, webhooks) through Ruellia, a scale-to-zero, multi-channel messaging platform. The API never talks SMTP directly — it calls Ruellia’s REST API with a least-privilege app key.
Dev-only for now. This integration is scoped to the dev environment (
dev.frond.cntnus.app). Stage/prod promotion happens after dev is verified.
What is provisioned (dev)
Desired state lives in infra/config/ruellia.json; reproduce it with:
RUELLIA_BUILDER_TOKEN=<builder-token> node scripts/ruellia-setup.mjs| Resource | Value |
|---|---|
| Sending domain | dev.frond.cntnus.app (dom_63513a178a72, pending DNS verification) |
| Templates | frond-canvas-shared (tpl_30d4942134e577b0), frond-canvas-published (tpl_b528f523994f621f), frond-welcome (tpl_7bb00091fe8a60b7) |
| App key | frond dev key (tok_ad215b37a3870034), scopes messages:send, messages:read, templates:read; secret in dev account as frond/dev/ruellia-api-key |
Until dev.frond.cntnus.app verifies, mail sends from the verified platform
default notifications@app.stage.ruellia.cntnus.app.
DNS (apply once in the cntnus.app Route53 zone)
SPF: TXT dev.frond.cntnus.app -> "v=spf1 include:amazonses.com ~all".
DKIM: three CNAME <token>._domainkey.dev.frond.cntnus.app -> <token>.dkim.amazonses.com
(see ruellia.json for the tokens). DMARC:
TXT _dmarc.dev.frond.cntnus.app -> "v=DMARC1; p=none;".
Inbound MX: dev.frond.cntnus.app -> inbound-smtp.us-east-2.amazonaws.com (priority 10).
Bounce handling lives only on the bounce. subdomain
(MX bounce.dev.frond.cntnus.app -> feedback-smtp.us-east-2.amazonses.com plus its
SPF TXT) — never point the bare domain at feedback-smtp.
Then verify and poll until verified:true:
curl -X POST -H "Authorization: Bearer $BUILDER" \ $RUELLIA_BASE/v1/domains/dom_63513a178a72/verifycurl -H "Authorization: Bearer $BUILDER" $RUELLIA_BASE/v1/domainsAPI routes
All routes require a Cognito bearer token (except where noted):
POST /documents/{id}/share—{ "to": "friend@example.com", "message?": "..." }→202 { messageId, to, canvasUrl }. Links the published/view/<token>URL when published, else the app home.POST /documents/{id}/notify-publish—{ "to": "..." }→202. Requires the canvas to be published.POST /notifications—{ "targetUrl": "https://…", "event": "canvas.published", "data": {} }→202 { messageId }(HTTPS webhooks only).GET /messages/{messageId}→ Ruellia delivery status proxy.
Without a configured key the messaging routes return 503; a suppressed
recipient returns 400. The implementation is services/api/src/messaging.ts
(pure fetch, no extra dependencies) with unit tests in
services/api/src/__tests__/messaging.test.ts.
REST examples (app key, server-side only)
# Templated share email (verified contract: channel + nested payload)curl -X POST -H "Authorization: Bearer $RUELLIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"channel":"email","email":{"to":"a@b.co","templateId":"tpl_30d4942134e577b0", "templateData":{"sharer":"Alice","canvasName":"C","canvasUrl":"https://…"}}}' \ $RUELLIA_BASE/v1/messages/send
# Delivery statuscurl -H "Authorization: Bearer $RUELLIA_API_KEY" \ $RUELLIA_BASE/v1/messages/msg_…
# SMS (E.164, 160 chars = 1 billed segment)curl -X POST -H "Authorization: Bearer $RUELLIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"channel":"sms","sms":{"to":"+14155552671","body":"hi"}}' \ $RUELLIA_BASE/v1/messages/sendDeploy wiring (dev)
The Lambda reads RUELLIA_BASE_URL, RUELLIA_FROM_EMAIL,
RUELLIA_*_TEMPLATE_ID (from infra/config/environments.json → dev.ruellia,
CDK context keys of the same name override) and resolves the key at runtime
from Secrets Manager (dev.ruellia.secretName). Runtime resolution keeps
deploys green before the secret exists and activates messaging without a
redeploy once it does.
Create the secret once in the dev account (692662505171):
aws secretsmanager create-secret --region us-east-1 \ --name frond/dev/ruellia-api-key --secret-string 'rk_live_…'No extra deploy flags are needed — the pipeline synth picks up
dev.ruellia automatically. CDK overrides (e.g. for stage later):
cd infra && npx cdk deploy -c env=dev \ -c ruelliaApiKeySecretName=frond/dev/ruellia-api-key \ -c ruelliaShareTemplateId=tpl_30d4942134e577b0 …The builder token (agent:full) is used only by the integrator running
ruellia-setup.mjs — it must never appear in code, config, or git.
Rate limit is 120 req/min per token (429 + Retry-After): the API maps
provider 429s to 502 so clients back off and retry.