Semaphor
Self-Hosting

Reports, Briefings, and Scheduled Delivery

Configure the AWS report scheduler, S3 artifact storage, PDF generation, and outbound email for self-hosted Semaphor

Self-hosted Semaphor uses the Semaphor Report Scheduler AWS SAM stack for PDF and CSV rendering, scheduled Automation execution, Briefing analysis, asynchronous exports, and outbound report email. The stack creates the Lambda functions, Step Functions state machines, EventBridge rules, and private S3 bucket these features require.

The app container is not the email sender

Setting RESEND_API_KEY in the Semaphor app does not configure Briefing or scheduled-report email. Those messages are sent by the scheduler stack. The app must have BRIEFINGS_EMAIL_SENDER_URL and the same LAMBDA_API_KEY used by that stack.

What this setup enables

  • On-demand dashboard and document-sheet PDF exports
  • Briefing and scheduled-report PDF or CSV attachments
  • Run now and recurring delivery by email
  • AI-generated Briefing analysis
  • EventBridge-driven Automation execution
  • Asynchronous large CSV exports

Prerequisites

  • A working self-hosted Semaphor deployment with a public HTTPS URL
  • AWS CLI credentials that can deploy CloudFormation, Lambda, IAM, S3, Step Functions, EventBridge, and CloudWatch resources
  • AWS SAM CLI, Docker, Node.js 22, and npm on the machine used to deploy the scheduler
  • Either:
    • an SES verified sender in the deployment region, with production access for unrestricted recipients; or
    • a Resend account and verified sender domain
  • An OpenAI API key if you want AI-generated Briefings

1. Deploy the report scheduler

Clone and configure the scheduler:

git clone https://github.com/semaphor-analytics/report-scheduler.git
cd report-scheduler
cp .env.example .env

Generate a shared service key:

openssl rand -hex 32

The repository's .env.example is the canonical scheduler configuration. Replace .env with the complete template below. Required settings are active. Optional features and the alternative Resend provider are present but commented out so customers can discover them without accidentally enabling them.

# ===== Required: Semaphor connection =====
# Public HTTPS URL with no trailing slash. Lambda must be able to reach it.
SEMAPHOR_APP_URL=https://semaphor.yourdomain.com

# Generate with: openssl rand -hex 32
# Copy this exact value to LAMBDA_API_KEY in the Semaphor app environment.
LAMBDA_API_KEY=replace-with-the-generated-key

# ===== Required for email: choose one provider =====
# Amazon SES is the default. The sender must be verified in this region.
EMAIL_PROVIDER_MODE=SES
SES_REGION=us-east-1
SES_SENDER_EMAIL=reports@yourdomain.com

# To use the same-stack Resend provider instead of SES, change
# EMAIL_PROVIDER_MODE above to EXTERNAL and uncomment the three values below.
# SES_REGION and SES_SENDER_EMAIL remain present as deployment parameters but
# are not used in EXTERNAL mode.
# EMAIL_EXTERNAL_AUTH_SECRET=replace-with-another-random-secret
# RESEND_API_KEY=re_...
# RESEND_SENDER_EMAIL=reports@yourdomain.com

# ===== Optional: AI-generated Briefings =====
# Leave all four commented if you only use custom-message reports.
# INSIGHT_LOOP_MODEL_PROVIDER=openai
# INSIGHT_LOOP_MODEL=gpt-5.5
# INSIGHT_LOOP_REASONING_EFFORT=medium
# OPENAI_API_KEY=sk-...

SEMAPHOR_APP_URL must be the externally reachable HTTPS URL. Lambda uses it for Automation execution and Briefing progress, completion, and failure callbacks. Keep every variable in this one file; uncomment optional settings only when the corresponding feature is enabled.

Deploy the stack:

./deploy.sh

The first deployment may prompt for AWS region and CloudFormation capabilities. Keep the scheduler and Semaphor app in the same region unless you have a deliberate cross-region design.

2. Record the stack outputs

sam list stack-outputs --stack-name semaphor-report-scheduler

Record these outputs. Whether each value is required depends on the capability you enable:

Stack outputSemaphor app variableRequired when
S3BucketNameS3_EXPORTS_BUCKETUsing Briefings, scheduled reports, or asynchronous exports
GeneratePdfFunctionUrlPDF_FUNCTION_URLGenerating PDF or CSV attachments and on-demand PDFs
EmailSenderFunctionUrlBRIEFINGS_EMAIL_SENDER_URLDelivering reports by email
InsightRunnerIngressFunctionUrlBRIEFINGS_RUNNER_URLUsing AI-generated Briefings
ExportStateMachineArnEXPORT_STATE_MACHINE_ARNUsing asynchronous large exports

Keep the Function URLs private as configuration values. The functions also validate the shared LAMBDA_API_KEY where required.

3. Give the Semaphor app access to S3

BRIEFINGS_ARTIFACT_STORAGE=s3 makes the app write Briefing and scheduled-report run artifacts under briefings/ in the scheduler bucket. The app therefore needs AWS credentials with access to that prefix.

On EC2, attach an instance profile to the Semaphor instance. Use long-lived access keys only when an IAM role is not available. A minimum policy for Briefing artifacts is:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SemaphorBriefingArtifacts",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::YOUR_S3_BUCKET/briefings/*"
    }
  ]
}

If you also enable asynchronous exports, add these entries to the policy's Statement array:

[
  {
    "Sid": "SemaphorExportDownloads",
    "Effect": "Allow",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::YOUR_S3_BUCKET/exports/*"
  },
  {
    "Sid": "SemaphorStartExports",
    "Effect": "Allow",
    "Action": "states:StartExecution",
    "Resource": "YOUR_EXPORT_STATE_MACHINE_ARN"
  }
]

If the app runs in Docker on EC2 and cannot obtain instance-profile credentials, set the instance metadata response hop limit to 2, or provide credentials to the container through your secrets manager. Do not commit AWS keys to source control.

4. Configure the Semaphor app

Keep all Semaphor-side report settings in one block in semaphor.env. Required settings are active below; optional capabilities are present but commented out.

AWS_REGION=us-east-1
BRIEFINGS_ARTIFACT_STORAGE=s3
S3_EXPORTS_BUCKET=semaphor-reports-123456789012-semaphor-report-scheduler

PDF_FUNCTION_URL=https://example.lambda-url.us-east-1.on.aws/
BRIEFINGS_EMAIL_SENDER_URL=https://example.lambda-url.us-east-1.on.aws/

# Must exactly match the scheduler stack value
LAMBDA_API_KEY=replace-with-the-generated-key

# Optional: AI-generated Briefings
# BRIEFINGS_RUNNER_URL=https://example.lambda-url.us-east-1.on.aws/

# Optional: asynchronous large exports
# EXPORT_STATE_MACHINE_ARN=arn:aws:states:us-east-1:123456789012:stateMachine:example

# Optional: use only when the app has no EC2 instance profile or workload role.
# Prefer injecting these through a secrets manager instead of storing them here.
# AWS_ACCESS_KEY_ID=AKIA...
# AWS_SECRET_ACCESS_KEY=...
# AWS_SESSION_TOKEN=...

When the app does not run with an EC2 instance profile, uncomment the AWS credential variables and provide them through your deployment's secret-management path.

Restart the app container after changing the environment:

docker-compose down
docker-compose up -d

5. Verify configuration without printing secrets

From the directory containing docker-compose.yml, run:

docker-compose exec api-service node -e '
const names = [
  "AWS_REGION",
  "S3_EXPORTS_BUCKET",
  "PDF_FUNCTION_URL",
  "BRIEFINGS_EMAIL_SENDER_URL",
  "LAMBDA_API_KEY"
];
for (const name of names) {
  console.log(`${name}=${process.env[name] ? "configured" : "MISSING"}`);
}
for (const name of ["BRIEFINGS_RUNNER_URL", "EXPORT_STATE_MACHINE_ARN"]) {
  console.log(`${name}=${process.env[name] ? "configured" : "not configured (optional)"}`);
}
console.log(`BRIEFINGS_ARTIFACT_STORAGE=${process.env.BRIEFINGS_ARTIFACT_STORAGE || "production default (s3)"}`);
'

Then test in this order:

  1. Export a dashboard or document sheet to PDF.
  2. Create a custom-message report with one email recipient and one PDF attachment, then click Run now.
  3. Create an AI-generated Briefing and run it.
  4. Create a recurring schedule and confirm the next EventBridge-driven run.

This order separates PDF, storage, email, runner, and scheduler problems.

Troubleshooting

BRIEFING_ARTIFACT_STORAGE_FAILED

The app could not write a generated artifact to S3. Check the S3_EXPORTS_BUCKET value, AWS_REGION, container credentials, and s3:PutObject permission on briefings/*.

docker-compose logs api-service | grep -A 12 "\[briefings\] Artifact storage failed"

The structured log includes the AWS error name, status code, storage mode, and whether the bucket was configured.

BRIEFINGS_EMAIL_SENDER_URL is not configured

Set BRIEFINGS_EMAIL_SENDER_URL to the scheduler stack's EmailSenderFunctionUrl output, set LAMBDA_API_KEY to the same value used during scheduler deployment, and recreate the app container. RESEND_API_KEY in semaphor.env does not replace this URL.

Email sender returns 401 or 403

Confirm that LAMBDA_API_KEY matches exactly in the scheduler stack and Semaphor app. Redeploy the scheduler after changing its key, then restart the app.

SES accepts the request but email is not delivered

  • Verify the sender identity in the same region configured by SES_REGION.
  • If the SES account is still in the sandbox, verify each recipient or request production access.
  • Review EmailSenderFunction logs in CloudWatch.
sam logs -n EmailSenderFunction --stack-name semaphor-report-scheduler --tail

BRIEFINGS_RUNNER_URL is not configured

Set BRIEFINGS_RUNNER_URL to InsightRunnerIngressFunctionUrl. If the runner starts but analysis fails, verify that the scheduler was deployed with a valid OPENAI_API_KEY and review both Insight Runner Lambda logs.

PDF attachment generation fails

Set PDF_FUNCTION_URL to GeneratePdfFunctionUrl, then verify GeneratePdfFunction logs. The Function URL must be reachable from the Semaphor app.

A recurring report does not start

Confirm the scheduler stack's Automation EventBridge rule is enabled and that Lambda can reach SEMAPHOR_APP_URL over HTTPS. Review AutomationDispatcherFunction logs.

sam logs -n AutomationDispatcherFunction --stack-name semaphor-report-scheduler --tail

S3 retention

The scheduler stack configures lifecycle rules for its pdfs/, emails/, and exports/ prefixes. Configure an S3 lifecycle rule for briefings/ that matches your organization's retention requirements. Keep the bucket private; artifact access should continue through Semaphor's permission-checked routes.

On this page