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.
| Variable | What it is |
|---|---|
S3_ENDPOINT | The 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_REGION | The region name the provider expects. Use the value from your provider's documentation. |
S3_BUCKET | A private bucket, dedicated to Areca or shared using a separate installation prefix. |
S3_PREFIX | Optional installation path, such as areca or sites/production. Unset or exactly empty uses the bucket root. |
S3_ACCESS_KEY and S3_SECRET_KEY | Credentials with read, write and delete access to the bucket. |
S3_FORCE_PATH_STYLE | Set 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):
- Stop all CMS instances and maintenance writers, including cleanup timers and authoring imports. Keep them stopped throughout the copy and configuration change.
- Back up a consistent database and its matching objects, plus the old environment and encryption keys.
- 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_keyand pendingstorage_object_deletion.storage_keyrows; 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. - 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.
- 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:
pnpm --filter @areca/cms s3:initThe 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
Authorizationheader on asset requests. - Respect the
Cache-Controlheader from the origin. The CMS sendspublic, max-age=31536000, immutablefor 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.
Reset links in the queue
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:
| Provider | Host | Port | Security | Username | Password |
|---|---|---|---|---|---|
| Resend | smtp.resend.com | 465 or 587 | tls on 465, starttls on 587 | resend | Your Resend API key |
| Postmark | smtp.postmarkapp.com | 587 | starttls | Your server API token | The same server API token |
| Amazon SES | email-smtp.<region>.amazonaws.com | 587 | starttls | Your SMTP user name | Your SMTP password |
| Mailgun | smtp.mailgun.org, or smtp.eu.mailgun.org for EU domains | 587 | starttls | postmaster@ your sending domain | Your domain's SMTP password |
| Google Workspace | smtp.gmail.com | 587 | starttls | Your full Google address | An 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.
-
Verify the sending domain in your provider's dashboard. The provider shows the DNS records to add.
-
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.
-
Add the DKIM records. The provider gives you one or more records. They let receivers check that the message was not changed in transit.
-
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.examplep=quarantinetells receivers to treat messages that fail SPF and DKIM as suspicious. Some teams start withp=nonewhile they read the reports sent to theruaaddress. -
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.
Related
- Configuration: every storage and email variable.
- Agency mail: set up the mail server in the CMS, and read the error messages.
- Asset retention: when stored files are removed.
- Production: the checklist before you go live.
- Troubleshooting: common storage and email errors.
Sign-in methods
Turn on email and password, GitHub, Google, Discord or Microsoft Entra ID sign-in, and keep registration invite-only.
Deployment targets (Dokploy and Cloudflare)
Connect Dokploy or Cloudflare to Areca, store their credentials safely, and understand how each Go live build receives its snapshot and passes its health check.