PurrOSDocs
Getting started

Email (SMTP) & file storage (S3)

Send email through any SMTP server, and store files on local disk or in any S3-compatible object storage.

PurrOS sends email through any SMTP server, and stores files either on local disk or in any S3-compatible object storage. Both are set with environment variables (see Configuration) and can be tested with the CLI.


Email (SMTP)

What PurrOS sends by email

EmailSent toStatus
Invitations, magic sign-in links, password resetsThe person signing inSent today
Security notices (new device, 2FA changed, account locked)The account ownerPlanned
Schedule published or changed, shift remindersEmployees (if they chose email)Planned
Approval requests and decisions (timesheets, time off, swaps, purchase orders)Managers and employeesPlanned
Alerts (overdue checklists, cash over/short, low stock, failed integrations)Chosen rolesPlanned (alerts are webhooks today)
Scheduled reports (PDF, CSV or Excel attached)Report recipientsPlanned
Purchase orders (PDF + CSV)Suppliers, when sending orders by emailPlanned
Customer invoices (PDF)CustomersPlanned (the PDF is available at GET /invoices/{id}/pdf)
AnnouncementsEmployees who chose emailPlanned

Once notifications ship, everyone will choose which ones they get by email. Security emails and sign-in links will always be sent.

Settings

VariableRequiredDefaultDescription
SMTP_HOSTYesSMTP server hostname
SMTP_PORTNo587587 (STARTTLS), 465 (implicit TLS) or 25
SMTP_SECURENofalsetrue for implicit TLS (port 465). On 587, STARTTLS is used automatically.
SMTP_USER, SMTP_PASSWORDUsuallyCredentials. Many providers use an API key as the password.
SMTP_FROMYesSender, e.g. Acme Operations <[email protected]>
SMTP_REPLY_TONoWhere replies go, e.g. an HR or support mailbox
SMTP_REQUIRE_TLSNotrueRefuse to send if the server doesn't support TLS
SMTP_TLS_REJECT_UNAUTHORIZEDNotrueSet false only for internal servers with self-signed certificates
SMTP_RATE_PER_SECONDNo10Stay under your provider's sending limit

Emails are sent by the worker from a queue, so a slow or unavailable SMTP server never slows the app down. Failed sends are retried with growing delays for up to 24 hours. Once sent, the email body is deleted; only the recipient, subject, kind and status are kept.

Example config/purros.env

SMTP_HOST=smtp.your-provider.example
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=your-smtp-password-or-api-key
SMTP_FROM="Acme Operations <[email protected]>"
SMTP_REPLY_TO=[email protected]

Any SMTP service works: a transactional email provider, Microsoft 365 or Google Workspace SMTP relay, Amazon SES, or your own mail server.

Make sure email gets delivered

To keep PurrOS email out of spam folders, set these DNS records for the domain in SMTP_FROM, following your email provider's instructions:

  • SPF: authorizes your provider to send for your domain.
  • DKIM: signs messages so receivers can verify them.
  • DMARC: tells receivers what to do with unauthenticated mail.

Test it

docker compose exec api purros email test --to [email protected]

It shows the exact SMTP error if sending fails.

Delivery log

purros email log [--limit 30] lists recent emails with their status (queued, sent, failed) and the reason for failures. Email content is not kept once sent, only the recipient, subject and type.

Branding and language

Emails use the company name. (Planned: the company logo, each recipient's language, and editable templates.)

Without SMTP

PurrOS works without email, but:

  • invitation and sign-in links are printed instead (purros users invite, purros users sign-in-link) and have to be shared by hand
  • magic-link sign-in and password reset by email are unavailable

File storage (S3)

What PurrOS stores as files

Anything uploaded through attachments: employee documents (contracts, IDs, certificates), photos (checklist, audit, repair ticket and waste photos), receipts and supplier invoices, and payslip PDFs. (Planned: kiosk photos, the shared files library, imports and full company exports, and branding images. "Export my data" ZIPs and invoice PDFs are generated on request and not stored.)

The database only keeps each file's metadata (name, type, size, checksum, owner, who can see it). The file itself lives in storage.

Drivers

STORAGE_DRIVERWhere files goGood for
local (default)$PURROS_STATE_DIR/files (the state volume in Docker)Single-server installs, evaluation
s3Any S3-compatible object storageProduction, multiple app servers, large volumes, easier backups

S3-compatible services include AWS S3, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces, Google Cloud Storage (interoperability mode), and self-hosted MinIO, Garage or Ceph.

S3 settings

VariableRequiredDefaultDescription
STORAGE_DRIVERlocalSet to s3
STORAGE_S3_BUCKETYesBucket name
STORAGE_S3_REGIONYesRegion, e.g. eu-central-1 (auto for some providers)
STORAGE_S3_ENDPOINTNon-AWSe.g. https://s3.eu-central-003.backblazeb2.com or http://minio:9000
STORAGE_S3_ACCESS_KEY_ID, STORAGE_S3_SECRET_ACCESS_KEYUsuallyCredentials. Leave empty on AWS to use an instance or task IAM role.
STORAGE_S3_FORCE_PATH_STYLENofalsetrue for MinIO and some other self-hosted services
STORAGE_S3_PREFIXNoFolder prefix inside the bucket, e.g. purros/, to share a bucket
STORAGE_S3_SSENoServer-side encryption: AES256 or aws:kms
STORAGE_S3_KMS_KEY_IDNoKMS key when using aws:kms
STORAGE_SIGNED_URL_TTLNo300Seconds a download link stays valid
STORAGE_MAX_UPLOAD_MBNo25Largest single upload

Example: AWS S3

STORAGE_DRIVER=s3
STORAGE_S3_BUCKET=acme-purros-files
STORAGE_S3_REGION=eu-central-1
STORAGE_S3_ACCESS_KEY_ID=AKIA...
STORAGE_S3_SECRET_ACCESS_KEY=...
STORAGE_S3_SSE=AES256

Example: self-hosted MinIO next to PurrOS

Add to docker-compose.yml:

  minio:
    image: minio/minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: purros
      MINIO_ROOT_PASSWORD: change-me-long-password
    volumes:
      - minio-data:/data
    restart: unless-stopped

volumes:
  minio-data:
STORAGE_DRIVER=s3
STORAGE_S3_ENDPOINT=http://minio:9000
STORAGE_S3_REGION=us-east-1
STORAGE_S3_BUCKET=purros
STORAGE_S3_FORCE_PATH_STYLE=true
STORAGE_S3_ACCESS_KEY_ID=purros
STORAGE_S3_SECRET_ACCESS_KEY=change-me-long-password

Create the bucket once in the MinIO console (port 9001) or with purros storage init.

Bucket setup

  • Keep the bucket private. PurrOS never makes files public. Downloads use short-lived signed URLs created only after checking that the person may see the file.
  • Turn on versioning, so deleted or overwritten files can be recovered.
  • Encryption at rest: use STORAGE_S3_SSE, or the provider's default bucket encryption.
  • Lifecycle rules are optional. Don't set rules that delete files PurrOS still references. (Retention settings that let PurrOS delete old files itself, e.g. kiosk photos after 90 days, are planned.)
  • Minimum permissions for the access key, on the bucket and prefix only: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket, s3:AbortMultipartUpload.

Example IAM policy for AWS:

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::acme-purros-files" },
    { "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"],
      "Resource": "arn:aws:s3:::acme-purros-files/*" }
  ]
}

How uploads and downloads work

  • Uploads go through the API (POST /api/v1/attachments). They are streamed straight to storage without being held in memory. PurrOS checks the file type from its content, enforces STORAGE_MAX_UPLOAD_MB, and records the SHA-256 checksum. See Attachments.
  • Downloads go through a permission check. With S3, the response is a redirect to a signed URL that expires after STORAGE_SIGNED_URL_TTL seconds, so the file comes straight from the bucket. With local storage, PurrOS streams the file itself.
  • Direct browser-to-bucket uploads (signed upload URLs) are planned. Until then, the bucket needs no CORS settings.

Test it

  • purros storage test writes, reads and deletes a test file (--backups tests the backup bucket)
  • purros storage verify [--checksums] checks that every attachment's file is in storage
  • purros doctor includes the same write/read test

Moving from local disk to S3

# 1. Add the S3 settings to config/purros.env, but keep STORAGE_DRIVER=local for now
docker compose exec api purros storage migrate --to s3
# 2. When it reports "0 remaining", switch the driver
#    STORAGE_DRIVER=s3
docker compose up -d
docker compose exec api purros storage verify

storage migrate copies every file and checks its checksum. It can be stopped and run again: files already copied are skipped. Run it once more right before switching STORAGE_DRIVER to catch files uploaded in the meantime. The local volume isn't deleted, so remove it yourself once you've confirmed everything works.


Database backups to S3 (optional)

PurrOS can upload every backup (scheduled and manual) to an S3-compatible bucket, and keeps a set number there. Use a different bucket from file storage, ideally with another provider or in another region.

VariableDefaultDescription
PURROS_BACKUP_S3_ENABLEDfalseUpload backups to the bucket below
PURROS_BACKUP_S3_BUCKET, PURROS_BACKUP_S3_REGION, PURROS_BACKUP_S3_ENDPOINT, PURROS_BACKUP_S3_ACCESS_KEY_ID, PURROS_BACKUP_S3_SECRET_ACCESS_KEY, PURROS_BACKUP_S3_FORCE_PATH_STYLE, PURROS_BACKUP_S3_SSE, PURROS_BACKUP_S3_KMS_KEY_IDSame meaning as the STORAGE_S3_* settings
PURROS_BACKUP_S3_PREFIXpurros-backups/Folder inside the bucket
PURROS_BACKUP_S3_KEEPPURROS_BACKUP_KEEPHow many backups to keep in the bucket

With S3 enabled, PURROS_BACKUP_DIR becomes optional:

  • With a directory, backups are kept locally and uploaded.
  • Without one, backups are only uploaded; the local copy is deleted once the upload succeeds.

A failed upload is recorded, shown by purros backup list and purros doctor, and retried with the next backup.

purros backup list --remote                 # backups in the bucket
purros backup restore s3:<name> --replace   # restore straight from the bucket
purros backup download <name>               # or download first
purros storage init --backups               # create the bucket if needed

Encrypt backups before they leave the server with PURROS_BACKUP_PASSPHRASE. See Backups & upgrades.

With S3 file storage, the files themselves are backed up through bucket versioning and replication or your own tools, unless you set PURROS_BACKUP_FILES=true to include them in backups. With local storage they're included by default.

On this page