Overview
Start Here
Installation
Tutorial
Architect App Builder
Todo List App
Beacon Beats Audio Store
UCAL App
Authoring
Dynamic Apps
Static Apps
dev_docs
Why Pharos Feels Different
Architect Builder
Framework
Artifacts & Layout
Configuration
Outbound Email
How to Use CLI
Contracts
YAML Authoring
Action Hooks and Middleware
Feature Transaction Primitives
Generated Surfaces
Declarative CRM
Input Validation
Identity and Tenancy
UI Surface Library
Social Connections
Payment Provider Management
Integration Adapters
Reference
Contract, UI, and Surface Reference
Styling UI Elements
Architect Media Artifacts
API Docs
Use Cases
Dynamic SPAs
Generated Admin and APIs
Scheduling and Marketplaces
Multi-App Hosting
Release Notes
Vision
Pharos Framework

← Back

Outbound Email Contract

Pharos sends transactional email through a framework-owned SMTP client implemented in the native runtime.

Purpose

Outbound email configuration lives in shared runtime config with optional app-local overrides. The sections below cover precedence, supported SMTP settings, verification-link origin rules, and delivery logging and persistence behavior.

Runtime ownership

Pharos owns:

  • SMTP connection setup
  • RFC 822 message assembly
  • TLS mode selection
  • auth with the configured relay
  • bounded error handling
  • delivery status persistence for framework-owned flows

Apps own:

  • sender overrides when needed
  • recipient selection through framework flows
  • app-owned outbound email templates under templates/email/*.yaml
  • user-facing template variables where the framework exposes them

Config keys

Shared runtime config from PHAROS_CONFIG

The shared runtime config file selected by PHAROS_CONFIG may define server-default email settings:

# outbound email is disabled by default until SMTP settings are uncommented
# uncomment these values and restart the Pharos runtime after configuration
# all SMTP keys below are explicit; Pharos does not alias them to other config keys
# smtp_server=<your_primary_smtp_server>
# smtp_encryption=STARTTLS
# smtp_port=587
# smtp_mail_from=noreply@example.com
# smtp_login=<your_primary_smtp_login>
# smtp_pass=PHAROS_SMTP_PASS
# smtp_timeout_seconds=15
# smtp_max_message_bytes=1048576
# optional secondary relay. this relay is inactive until smtp2_* values are set.
# smtp2_server=<your_secondary_smtp_server>
# smtp2_encryption=STARTTLS
# smtp2_port=587
# smtp2_mail_from=noreply@example.com
# smtp2_login=<your_secondary_smtp_login>
# smtp2_pass=PHAROS_SMTP_PASS
# smtp2_timeout_seconds=15
# smtp2_max_message_bytes=1048576
# supported SMTP mode pairs for both relays:
# - smtp_encryption=STARTTLS requires smtp_port=587
# - smtp_encryption=SSL requires smtp_port=465
# - smtp2_encryption=STARTTLS requires smtp2_port=587
# - smtp2_encryption=SSL requires smtp2_port=465
base_url=app.example.com

Supported framework-level keys:

Primary relay:

  • smtp_server=<relay host>
  • smtp_port=587|465
  • smtp_mail_from=<sender address> required when outbound email is enabled
  • smtp_login=<smtp username>
  • smtp_pass=<environment variable name>
  • smtp_encryption=STARTTLS|SSL
  • smtp_timeout_seconds=<positive integer>
  • smtp_max_message_bytes=<positive integer>

Optional secondary relay:

  • smtp2_server=<relay host>
  • smtp2_port=587|465
  • smtp2_mail_from=<sender address>
  • smtp2_login=<smtp username>
  • smtp2_pass=<environment variable name>
  • smtp2_encryption=STARTTLS|SSL
  • smtp2_timeout_seconds=<positive integer>
  • smtp2_max_message_bytes=<positive integer>

Shared URL origin:

  • base_url=<host or absolute origin>

App-local overrides in app.conf

Apps may override all supported email keys in app.conf. An enabled primary SMTP configuration must still provide an explicit smtp_mail_from value after precedence resolution. If a secondary relay is configured, its smtp2_* keys follow the same precedence model independently.

Example:

base_path=/your-app
base_url=app.example.com
smtp_server=<your_smtp_server>
smtp_port=587
smtp_mail_from=app_noreply@example.com
smtp_login=<your_app_smtp_login>
smtp_pass=YOUR_APP_SMTP_PASS
smtp_encryption=STARTTLS
smtp_timeout_seconds=15
smtp_max_message_bytes=1048576

Resolution precedence

For each supported email key, Pharos resolves values in this order:

  1. app-local override from app.conf
  2. server default from the config file selected by PHAROS_CONFIG
  3. framework default only where the wider runtime already allows one and only for that same explicit key

This precedence applies independently to the primary smtp_* keys and the optional secondary smtp2_* keys.

Verification-link origin precedence

Verification links are resolved in this order:

  1. app-local base_url
  2. server-default base_url
  3. request-derived origin fallback

If base_url is provided as a bare host such as app.example.com, Pharos normalizes it to https://app.example.com for outbound verification links.

SMTP behavior

Pharos supports exactly two relay mode pairs for both the primary and optional secondary relay:

  • STARTTLS on port 587
  • SSL on port 465

If an explicit SMTP configuration provides a mismatched mode and port pair, the runtime rejects that configuration instead of silently correcting it.

When both relays are configured, Pharos attempts the primary relay first and only attempts the secondary relay when the primary delivery attempt does not succeed. The secondary relay remains inactive when its smtp2_* keys are unset, empty, or null.

For transient transport failures only, Pharos applies one bounded in-request retry per relay with a fixed one-second backoff. Terminal config, auth, validation, and unsupported-mode failures are not retried.

The framework validates sender, recipient, and subject header safety before attempting transport. Message bodies are bounded by smtp_max_message_bytes.

Secret handling

SMTP secrets should be provided through environment variables rather than checked-in config. By default the examples reuse the same secret env var for both relays, though operators may choose different env vars if needed.

Example:

export PHAROS_SMTP_PASS='your relay password'

Do not store SMTP passwords in repo-tracked config, templates, or docs examples.

App-owned template contract

Pharos loads outbound email templates from the active app artifact at:

  • templates/email/<template_id>.yaml

Each template currently supports these top-level YAML keys:

  • subject
  • body

Variable substitution uses bounded {{name}} placeholders.

Current framework-owned template ids include:

  • verify_email
  • business_invite

Delivery persistence

Framework-owned verification and invite delivery writes state into notification_deliveries. Expected delivery states include:

  • queued
  • retryable during a bounded in-request retry window
  • succeeded
  • skipped
  • failed

The same notification_delivery row also records bounded retry metadata such as:

  • attempt_count
  • max_attempts
  • backoff_seconds
  • last_error
  • last_smtp_code
  • next_attempt_at
  • updated_at
  • completed_at

Framework-owned signup verification tokens are persisted in email_verifications.

Logging contract

Delivery failures and skipped deliveries are written to the resolved app log path, typically:

  • /var/log/pharos/<app_id>.log

These log lines include bounded metadata such as:

  • app id
  • relay name such as primary or secondary
  • template id
  • subject
  • recipient
  • stable reason label
  • bounded transport error text

When both relays are configured, Pharos records bounded email_delivery_attempt, email_delivery_retry, and final relay outcome events in service logs so relay attempt order is visible without exposing secrets. App-log routing may also include the same events when that surface is enabled in the current deployment.

They do not include:

  • SMTP passwords
  • full message bodies

Project policy currently disables .error.jsonl sidecar emission for outbound email events so logging stays consolidated in the .log files.

Provider example

A managed SMTP relay works with this contract using authenticated settings such as:

  • smtp_server=<your_smtp_server>
  • smtp_port=587
  • smtp_encryption=STARTTLS

Brevo is one compatible example, but the framework remains provider-agnostic as long as the relay supports standard authenticated SMTP.

Operator notes

Keep relay setup explicit and bounded:

  • DNS for the configured relay host must resolve from the Pharos runtime host before delivery can succeed
  • SMTP auth failures should be treated as config or credential defects, not as a signal to silently fall back to unrelated keys
  • smtp_timeout_seconds and smtp2_timeout_seconds should stay low enough to fail fast on dead relays without stalling request handling longer than necessary
  • use STARTTLS only with port 587
  • use SSL only with port 465
  • when both relays are configured, expect service-log ordering of email_delivery_attempt for primary, then secondary only if the primary attempt did not succeed

Private-network deployment notes

When Pharos runs in a private VPC without direct internet egress:

  • configure Cloud NAT or equivalent outbound egress
  • allow egress to TCP 587 for STARTTLS
  • allow egress to TCP 465 when SSL is needed
  • configure SPF and DKIM for the sender domain
  • use a stable public egress IP only when the relay provider requires IP authorization