Skip to content

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:

Terminal window
RUELLIA_BUILDER_TOKEN=<builder-token> node scripts/ruellia-setup.mjs
ResourceValue
Sending domaindev.frond.cntnus.app (dom_63513a178a72, pending DNS verification)
Templatesfrond-canvas-shared (tpl_30d4942134e577b0), frond-canvas-published (tpl_b528f523994f621f), frond-welcome (tpl_7bb00091fe8a60b7)
App keyfrond 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:

Terminal window
curl -X POST -H "Authorization: Bearer $BUILDER" \
$RUELLIA_BASE/v1/domains/dom_63513a178a72/verify
curl -H "Authorization: Bearer $BUILDER" $RUELLIA_BASE/v1/domains

API 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)

Terminal window
# 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 status
curl -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/send

Deploy 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):

Terminal window
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):

Terminal window
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.