Areca Docs

Storage and email

Configure S3-compatible storage for uploads, serve assets through a CDN, and send email through SMTP, with provider settings and delivery checks.

Areca needs an S3-compatible bucket for uploaded images and files. It also needs an SMTP server to send email, such as invitations. The bucket is required at startup. The mail server is optional: without one, mail is off until you set one up. This page explains how each one is used, and how to check that it works. For every variable, see configuration.

S3-compatible storage

Every uploaded asset and branding file is stored in one bucket. The CMS reads and writes the bucket with the access key you provide. Browsers never connect to the bucket directly.

VariableWhat it is
S3_ENDPOINTThe full URL of the storage API, for example https://s3.eu-central-1.example.com or http://rustfs:9000 in the Docker Compose stack.
S3_REGIONThe region name the provider expects. Use the value from your provider's documentation.
S3_BUCKETA private bucket, dedicated to Areca or shared using a separate installation prefix.
S3_PREFIXOptional installation path, such as areca or sites/production. Unset or exactly empty uses the bucket root.
S3_ACCESS_KEY and S3_SECRET_KEYCredentials with read, write and delete access to the bucket.
S3_FORCE_PATH_STYLESet to true for servers that need path-style URLs, such as MinIO and RustFS. The default is false.

Use a shared bucket

Set S3_PREFIX=areca to keep this installation's objects under areca/ in an existing shared bucket. Nested paths work too: S3_PREFIX=sites/production uses sites/production/. Surrounding slashes are removed, so /areca/ normalizes to areca. Leave the variable unset or exactly empty (S3_PREFIX=) for existing bucket-root behavior.

Nonempty values must contain a path: slash-only values, any whitespace or control characters, backslashes, repeated internal slashes (a//b), and . or .. segments are invalid. Invalid configuration fails before storage operations; it never becomes root mode.

Every object operation stays within the normalized prefix plus /: uploads, reads, deletes, branding, queued cleanup, authoring export/import and reset. With areca, neither areca-other/ nor root objects are used. There is no fallback to root for missing files. Database keys, authoring bundle keys and public asset URLs stay logical and unchanged; the prefix is applied exactly once at the S3 boundary.

Use the same S3_PREFIX on every CMS process and on standalone cleanup and authoring-transfer commands for that installation. db:reset deletes every object in the subtree, including manually placed files without database records. With the variable unset or exactly empty, reset still empties the entire bucket.

This setting scopes Areca's requests; it does not restrict what the credentials can access. Enforce access restrictions with the provider's policies and grant only the permissions needed. Bucket checks, creation and readiness remain bucket-level and do not probe read/write/delete rights within the prefix. Test an upload, read and delete before relying on the configuration.

Bucket-check permissions

Startup and readiness use HeadBucket. For AWS S3 general-purpose buckets, this requires s3:ListBucket on the bucket. A listing grant conditioned only on an object prefix does not cover this request: HeadBucket sends no prefix. A 403 makes startup retry until its deadline and fail; readiness also fails.

For an existing AWS bucket named company-projects and S3_PREFIX=areca, this identity policy allows bucket checks and limits object access to areca/:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::company-projects"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::company-projects/areca/*"
    }
  ]
}

The unrestricted bucket-listing grant also lets these credentials list other prefixes' object names, although their object access remains limited by the policy above. Other attached policies can grant more access. This example assumes the bucket already exists; it does not grant bucket creation or permissions for additional features such as customer-managed encryption keys. S3-compatible providers, including Hetzner, may use different permission models: check their HeadBucket, listing and object-access rules rather than assuming AWS IAM policies apply.

Move an existing installation

Changing S3_PREFIX does not move files. There is no automatic migration or fallback. For a move from root to areca/ (or between prefixes):

  1. Stop all CMS instances and maintenance writers, including cleanup timers and authoring imports. Keep them stopped throughout the copy and configuration change.
  2. Back up a consistent database and its matching objects, plus the old environment and encryption keys.
  3. Inventory and copy only this installation's Areca-owned objects, preserving each relative logical key under the destination prefix. Collect keys from asset.storage_key, branding.logo_key, branding.favicon_key and pending storage_object_deletion.storage_key rows; deduplicate them and omit null branding keys. Pending deletion keys may already be absent: preserve the queue and migrate any matching objects that still exist so cleanup can continue at the new location. Resolve source keys beneath the old prefix, if any, and destination keys beneath the new one. Do not recursively copy an entire shared bucket into a prefix within itself; that can include unrelated data or copy the destination back into itself.
  4. Verify object counts, bytes/checksums and representative asset and branding reads before deleting any originals. Keep originals and the backup until the move is accepted.
  5. Restart all CMS processes and maintenance writers with the same new prefix. Public URLs and database keys need no rewrite.

To roll back after new writes, stop all writers again and reverse-sync the installation's changes, including deletions, to the old location, or restore the consistent pre-change database and object snapshot together (losing later changes). Toggling the environment alone does not recover objects written at the new location.

Logical object keys are now validated in both root and prefix mode. Empty keys, empty path segments, . or .. segments, backslashes and control characters are rejected. Areca-generated keys already follow these rules. Inventory any legacy database or authoring-bundle keys before upgrading; unusual keys may need a separate repair. Invalid keys requested through the public asset endpoint return 404 without a storage request; direct adapter calls still reject them as errors.

Create the bucket

The bucket must exist before the CMS starts. You have two options:

  • Docker image. The container entrypoint checks for the bucket on every start and creates it if it is missing. You do not need to do anything extra.
  • Running from source. Run the bucket script from the repository root, with the storage variables in apps/cms/.env.local:
Terminal
pnpm --filter @areca/cms s3:init

The script checks for the bucket and creates it if it is missing. It is safe to run more than once.

The access key can read every upload

The S3 key pair has full access to every stored file. Keep it in your secrets store or host environment, not in the repository. Rotate it at the provider and update the CMS environment. Rotating the key does not touch the stored objects.

Public assets and the CDN

Uploaded files are public. The CMS serves each one at a URL under /api/assets/file/ without asking for a sign-in. The URL contains a random ID, so anyone who has the link can open the file. Links also appear in page source, referrer headers and shared links.

This applies to assets attached to drafts, unpublished entries and archived content, not only to live pages. Draft text is protected by sign-in and preview tokens, but its images are not. Do not upload embargoed or confidential material.

Keep the S3 bucket private. The CMS is the only thing that should reach it.

To serve assets through a CDN:

  • Point the CDN at the CMS origin, not at the bucket.
  • Cache only the /api/assets/file/ path. Do not cache other /api/ routes or the application's pages, because those depend on sign-in.
  • Forward only the path. Ignore cookies and the Authorization header on asset requests.
  • Respect the Cache-Control header from the origin. The CMS sends public, max-age=31536000, immutable for assets, because each file name is unique.

Keep APP_URL set to the CMS origin, not the CDN, unless your CDN rewrites asset URLs.

Assets stay in storage while any draft, revision, release or archive still refers to them. Read asset retention before you remove files.

Email (SMTP)

The CMS sends email through an SMTP server. The server's host, port and encryption come from the SMTP_* variables, or from Agency > Mail after you sign in. Settings saved in Agency > Mail replace the environment settings completely. For the variables, see configuration. For the screen, see agency mail.

If you set no server in either place, mail is off. Invitations still work, and you can share a link with Regenerate link on the pending invitation. Forgot password tells people to ask an agency admin, and security notices are not sent.

The sender's display name comes from the branding settings. The sender address comes from SMTP_FROM or from Agency > Mail.

What gets sent

  • Invitations to join a client.
  • Password reset links, from the sign-in page or from an agency admin.
  • Email verification links.
  • Security notices: password changed, two-factor authentication turned on or off, removed from a client, and role changed. These cannot be turned off. See your account and sign-in.
  • Notification emails: releases going live or failing, scheduled entries going live, rollbacks, and accepted invitations. People choose which ones they get. See notification emails.

What each role receives

  • Everyone gets account emails, such as invitations, password resets, verification links and security notices. They go to verified and unverified addresses alike.
  • Owners get most notification emails: releases going live or failing, scheduled releases that fail, rollbacks, and invitations they sent that were accepted. They can change their own choices, and set defaults for their client.
  • Editors get a notification email only when they started the release or scheduled the entries themselves. Owners publish, so most of these emails go to owners.
  • Agency admins also get Releases failing for any client, by default, for every client.

Notification emails go only to verified addresses. Without a mail server, Areca sends no notification emails. The full list, and how each person can choose, is in notification emails.

How sending works

When you invite someone, resend an invitation, or send a test, the CMS tries to send the email straight away, so you see the result at once.

If the try fails, the email is queued and tried again. Areca retries about 1 minute after the first try, and again about 5 minutes after that. If the last try fails too, the email is marked failed. Some errors will not be fixed by waiting, such as a rejected password or a refused recipient. Those fail at once, without retries.

Each connection has a 15-second timeout for connecting, for the server's greeting and for the socket. The CMS always verifies the server's certificate.

A status of sent means the mail server accepted the message. It does not confirm that the message reached an inbox.

A password reset link is a secret, so the queue does not keep it in plain text. When a key is set, Areca stores the reset token encrypted, using SECRETS_ENCRYPTION_KEY, or AI_SETTINGS_ENCRYPTION_KEY if that is unset. Generate the key with openssl rand -base64 32, and keep it with your database backups.

Without either key, Areca stores no token. A reset email is then tried once, straight away, and is not retried if that try fails. The person must ask for a new link.

Where failures show

  • A toast appears after you send an invitation, resend one, or send a test, if the send fails.
  • The People list shows Email not delivered on the pending invitation, with the reason in plain language. See people and invitations.
  • Recent emails in Agency > Mail lists the last 50 emails, with the status and error for each failure. Use the filter to show only failed emails.
  • A banner appears for agency admins after three emails in a row fail. It links to Recent emails.

Find details in the logs

Each send is logged under the mail component. Each entry includes the outbox ID, the kind of email, the attempt number, the host and port, and the mail server's response code. It never includes the SMTP password, the invitation link or other tokens.

To find the log entries for one email, copy its outbox ID from the row in Recent emails, or from the message in the UI, and search the logs for it.

Email providers

Most people send through a transactional email provider. Use the values from your provider's dashboard, because they depend on your account. These are the usual settings:

ProviderHostPortSecurityUsernamePassword
Resendsmtp.resend.com465 or 587tls on 465, starttls on 587resendYour Resend API key
Postmarksmtp.postmarkapp.com587starttlsYour server API tokenThe same server API token
Amazon SESemail-smtp.<region>.amazonaws.com587starttlsYour SMTP user nameYour SMTP password
Mailgunsmtp.mailgun.org, or smtp.eu.mailgun.org for EU domains587starttlspostmaster@ your sending domainYour domain's SMTP password
Google Workspacesmtp.gmail.com587starttlsYour full Google addressAn app password

Notes for each provider:

  • Resend uses the API key as the password. Create a key with permission to send.
  • Postmark uses the same server token for both the username and the password.
  • Amazon SES needs SMTP credentials, which you create in the SES console. Do not use your IAM access keys. Replace <region> with the region where you verified your domain.
  • Mailgun uses the SMTP credentials of the domain you send from. Use the EU host if the domain was created in the EU region.
  • Google Workspace needs an app password. Create one in your Google account, which requires 2-step verification to be on.

Set the sender address to an address on a domain you have verified with the provider, such as cms@agency.example.

Deliverability

Email sent from your domain has the best chance of reaching an inbox when the domain proves that the provider may send for it. Set this up once, before you invite clients.

  1. Verify the sending domain in your provider's dashboard. The provider shows the DNS records to add.

  2. Add the SPF record. This is a TXT record on the sending domain that lists the servers allowed to send for it. Use the value your provider gives you. A domain should have only one SPF record.

  3. Add the DKIM records. The provider gives you one or more records. They let receivers check that the message was not changed in transit.

  4. Add a DMARC record at _dmarc.<your domain>, for example:

    DNS TXT record for _dmarc.agency.example
    v=DMARC1; p=quarantine; rua=mailto:dmarc@agency.example

    p=quarantine tells receivers to treat messages that fail SPF and DKIM as suspicious. Some teams start with p=none while they read the reports sent to the rua address.

  5. Use a From address on that domain. Mail from a domain you have not verified can fail these checks, or be refused by the provider. If the provider refuses it, Areca shows the message in agency mail.

The DNS records take effect after they are published, so allow some time before you test. Your provider's dashboard shows when the domain is verified.

Test delivery

Test email before you invite real clients:

Set up the mail server. Either set the SMTP_* variables for the stack, or sign in and enter the server in Agency > Mail. For a local stack, the Docker Compose file points the CMS at a Mailpit container on port 1025. Mailpit's inbox is at http://localhost:8025.

Restart the CMS if you changed variables. Environment changes apply only after a restart. Settings saved in Agency > Mail apply straight away.

Send a test email. In Agency > Mail, select Verify connection, then Send test email. The message goes to your own address.

Invite a test address. Use an address you control, and invite it from the People list.

Check the inbox. Open the message, and check that the link opens the invitation page.

Check the result if it fails. Open Recent emails in Agency > Mail. It shows the reason for each failure. The CMS log has the same details under the mail component.

Also check the storage path. Upload a small image in the CMS, and confirm that it loads from /api/assets/file/ on your public CMS address.

On this page