# SFTPGo — Single-File AI Reference **Source of truth**: this file. Self-contained; paste it into any AI's context (ChatGPT, Gemini, Cursor, Copilot, Claude, …) before asking for help on SFTPGo REST API payloads, WebAdmin configuration, server deployment, environment variables, or Event Action templates. Verified against SFTPGo v2.7.x. **Scope covered** 1. Where the authoritative sources live (OpenAPI spec, public docs, GitHub). 2. REST API payload conventions the OpenAPI spec cannot fully express. 3. Deployment & configuration — install methods, config file format, environment-variable naming rule, `env.d/` recipes. 4. Event Action templates — context, function map, trigger-specific field availability, action-type sub-configs, pitfalls. 5. Workflow suggestions for AI agents producing SFTPGo artefacts. --- ## 0. Where to find more detail | Need | Source | | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Field names, enum values, schema shape, required fields | OpenAPI spec: `https://sftpgo.com/assets/openapi.yaml` (latest release) or the `openapi/openapi.yaml` shipped with the deployed installation (older but authoritative for that server). | | Operational how-to (install, storage, groups, shares, OIDC, tutorials, …) | Public docs at `https://docs.sftpgo.com/enterprise/` — mirrored at `https://github.com/sftpgo/docs` branch `enterprise`. Raw markdown: `https://raw.githubusercontent.com/sftpgo/docs/enterprise/docs/.md` | | Task → file cheat sheet for the public docs | `documentation-map.md` in the docs repo. | | Disambiguation of SFTPGo-specific terms | `glossary.md` in the docs repo. | | Ready-to-paste Event Action templates with both WebAdmin and REST forms | [`skills/sftpgo/references/examples.md`](skills/sftpgo/references/examples.md) in this repo. | When in doubt about a specific field, enum value, or required property: **read the OpenAPI spec**. This reference documents what the spec cannot easily encode (tagged unions, template semantics, cross-field dependencies). --- ## 1. REST API payload conventions These rules apply to every `/api/v2/*` endpoint and, by equivalence, to every WebAdmin form (the UI posts the same JSON shape). ### 1.1 Authentication - **JWT token** — `POST /api/v2/token` with admin HTTP Basic auth. Short-lived; refresh flow provided. - **API key** — created via `/api/v2/apikeys`; sent as header `X-SFTPGO-API-KEY: `. Preferred for non-interactive automation. - **OIDC** — only for interactive WebAdmin / WebClient login, not for REST API calls. ### 1.2 Tagged unions (the most common trap) Many fields use an integer selector to pick which sub-configuration is active; the other sub-objects are ignored. - `User.filesystem.provider` — `0` local, `1` S3, `2` GCS, `3` Azure Blob, `4` encrypted local, `5` SFTP, `6` HTTP, `7` FTP. Only the matching `*config` sub-object is read (`s3config`, `gcsconfig`, `azblobconfig`, `cryptconfig`, `sftpconfig`, `httpconfig`, `ftpconfig`). - `EventAction.options.type` — selects which of `http_config`, `cmd_config`, `email_config`, `retention_config`, `fs_config`, `pwd_expiration_config`, `idp_config`, `user_inactivity_config`, `imap_config`, `icap_config`, `share_expiration_config`, `event_report_config` applies. See §4 for the full enum. - `fs_config.type` (inside a filesystem action) — selects the operation (rename / delete / mkdirs / exist / compress / copy / pgp / metadata-check / decompress). Send the matching sub-object for the selected `type`. Other sub-objects are zeroed on save (the server enforces this). ### 1.3 Secret envelope Every secret field (user password, backend credentials, API keys, service account JSON, IMAP/SMTP passwords, etc.) uses an envelope: ```json {"status": "Plain", "payload": "your-secret"} ``` On read the server returns: ```json {"status": "AES-256-GCM", "payload": "", "key": "...", "additional_data": "..."} ``` To **preserve** the current secret on an update, send: ```json {"status": "Plain", "payload": "[**redacted**]"} ``` Any other payload replaces it. ### 1.4 Idempotency and update shape - `POST` creates every time; there is no built-in upsert. If the unique key already exists the endpoint returns `409 Conflict`. - `PUT /api/v2//` is a **full-object replace**. Omitted fields are reset to default. - Safe update pattern: `GET` → mutate in memory → `PUT`. When mutating only a few fields, make sure you round-trip the ones you did not touch. - Status codes: `201 Created` + `Location` header on creation, `200 OK` on update, `400` validation error (message is the actual reason — read it), `403` permission, `404` not found, `409` unique-key conflict. ### 1.5 User payload minimum (per backend) | Provider | Integer | Must set | Home dir required? | | -------------------- | ------- | -------------------------------------------------------------- | ------------------ | | Local | 0 | *(none — files live under `home_dir`)* | Yes | | S3 | 1 | `s3config.bucket`, `access_key` + `access_secret`, `region` | No | | Google Cloud Storage | 2 | `gcsconfig.bucket`, `credentials` (service account JSON) | No | | Azure Blob | 3 | `azblobconfig.container`, `account_name`, `account_key` | No | | Encrypted local | 4 | `cryptconfig.passphrase` | Yes | | SFTP backend | 5 | `sftpconfig.endpoint`, `username`, `password` or `private_key` | No | | HTTP filesystem | 6 | `httpconfig.endpoint` | No | Every user must have `permissions["/"]` set. Permissions are a map `virtual path → operation tokens`: `*` (all), `list`, `download`, `upload`, `overwrite`, `delete` (or `delete_files` + `delete_dirs`), `rename` (or `rename_files` + `rename_dirs`), `create_dirs`, `create_symlinks`, `chmod`, `chown`, `chtimes`, `copy`. Longest-prefix match wins. ### 1.6 Groups condensed | Type | JSON `type` | Count per user | Contributes | | ---------- | ----------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Primary | `1` | At most one | Base configuration (filesystem, home, quotas, bandwidth, password policy). Applied only when the user's field is 0/empty. | | Secondary | `2` | Any number | Additive: extra virtual folders, per-path permissions, IP filters, share policies, WebClient permissions. | | Membership | `3` | Any number | **Nothing is inherited.** Pure tag for event-rule conditions. | Placeholders accepted inside group values: `%username%`, `%role%`, `%custom1%…%customN%` (from `filters.custom_placeholders`). Apply in `home_dir`, `key_prefix`, `starting_directory`, virtual-folder `virtual_path`, SFTP backend `username`. ### 1.7 Shares Scope integers: `1` read, `2` write, `3` read+write. Options: password, expiration, IP allow-list, `options.auth_mode: 1` for email authentication. SFTPGo does **not** implement SAML — only OIDC for external identity providers. --- ## 2. Deployment & configuration ### 2.1 Install methods | Target | Method | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Debian / Ubuntu | APT repo `https://download.sftpgo.com/apt` — installs the `sftpgo` package and registers a systemd unit. | | RHEL / CentOS / Rocky / SUSE | YUM / zypper repo `https://download.sftpgo.com/yum/`. | | Windows | Signed installer or `winget install -e --id drakkan.SFTPGoEnterprise`. Registers a service automatically. | | Docker | Official registry (license required for Enterprise). | | Kubernetes | Official Helm chart. | | AWS / Azure / GCP | Marketplace listings (Starter / Premium). License bundled with subscription. | Without a valid license, SFTPGo runs in **limited mode**. Limits: 2 concurrent transfers, plugins disabled, local filesystem only, no PGP / ICAP / IDP account check / Event report actions. **Cloud marketplace offerings** ship preconfigured: license activated, audit-logs plugin enabled, in-memory transfer pipes on, data provider initialized for standalone use. The Starter marketplace tier allows **2 active plugins total** (audit-logs occupies one slot, leaving one free); Premium and higher tiers have no plugin limit. To add a third plugin on Starter, upgrade or disable audit to free its slot. ### 2.2 Config file location and format The binary looks for a file named `sftpgo.` in the configured *config directory*. Supported extensions (via viper): **JSON** (default shipped), **YAML**, **TOML**, **HCL**, **Java properties**, **envfile**. | Platform | Default config dir | | ------------------------ | ----------------------------------- | | Debian / RPM (systemd) | `/etc/sftpgo` | | Docker image | `/var/lib/sftpgo` (override with `-c`) | | Windows installer | `C:\ProgramData\SFTPGo Enterprise` | Override with `-c ` on any `sftpgo` subcommand. On Linux the systemd unit sources `/etc/sftpgo/sftpgo.env` and also reads every regular file under `/etc/sftpgo/env.d/` before starting. On Windows the service reads every regular file from `C:\ProgramData\SFTPGo Enterprise\env.d\` at startup. The `.env` suffix is only a convention — any file name is accepted. **Recommendation**: do not edit the shipped `sftpgo.json`. Keep it at defaults and put all overrides in env files under `env.d/` — package upgrades that ship a new default config cannot then conflict with your edits. **File config vs WebAdmin UI.** TLS certificates, ACME, SMTP, branding, OIDC, and LDAP are all exposed in both file config and the WebAdmin UI. **Prefer the UI when available**: it persists in the database (encrypted when appropriate), replicates across HA nodes, and most changes take effect without a service restart. Reserve file-based config for what the UI doesn't cover (bindings, transport-level options, hooks, memory pipes, licensing) or for headless deployments. For **OIDC** specifically: env-var config is acceptable if you only need a single binding; for multiple bindings prefer the UI — the `__N__` list-of-struct env-var syntax gets unwieldy fast. ### 2.3 Environment variable naming rule ```text SFTPGO_
__[__…] ``` - Prefix: `SFTPGO_` (one underscore). - Separator between struct levels: **double** underscore `__`. - Keys are uppercased JSON keys (`defender` → `DEFENDER`). Examples: | JSON path | Env var | | ------------------------------- | -------------------------------------- | | `common.defender.enabled = true` | `SFTPGO_COMMON__DEFENDER__ENABLED=true` | | `webdavd.cors.enabled = true` | `SFTPGO_WEBDAVD__CORS__ENABLED=true` | | `data_provider.driver = "postgresql"` | `SFTPGO_DATA_PROVIDER__DRIVER=postgresql` | | `telemetry.enable_profiler = true` | `SFTPGO_TELEMETRY__ENABLE_PROFILER=true` | ### 2.4 Lists Scalar lists are comma-separated: ```shell SFTPGO_COMMON__ACTIONS__EXECUTE_ON=upload,download ``` Lists of structs are indexed: ```shell SFTPGO_SFTPD__BINDINGS__0__PORT=22 SFTPGO_SFTPD__BINDINGS__0__ADDRESS=0.0.0.0 SFTPGO_SFTPD__BINDINGS__1__PORT=2022 ``` Indices must be contiguous: a gap (`__0__`, `__2__`) leaves index 1 unseen. ### 2.5 Env var precedence 1. **Process environment** (init system, `docker run -e`, `kubectl set env`). 2. **`/env.d/`** — every regular file in the directory is loaded at SFTPGo startup (the `.env` suffix in examples is only a convention — any file name works). Does **not** override existing env. 3. **`EnvironmentFile=/etc/sftpgo/sftpgo.env`** (Linux systemd) — loaded before the binary starts, acts like process env. ### 2.6 Common production recipes ```shell # Bind SFTP on port 22 SFTPGO_SFTPD__BINDINGS__0__ADDRESS= SFTPGO_SFTPD__BINDINGS__0__PORT=22 # PostgreSQL data provider SFTPGO_DATA_PROVIDER__DRIVER=postgresql SFTPGO_DATA_PROVIDER__NAME=sftpgo SFTPGO_DATA_PROVIDER__HOST=db.internal SFTPGO_DATA_PROVIDER__PORT=5432 SFTPGO_DATA_PROVIDER__USERNAME=sftpgo SFTPGO_DATA_PROVIDER__PASSWORD='your-password' SFTPGO_DATA_PROVIDER__SSLMODE=1 # Defender (intrusion detection) SFTPGO_COMMON__DEFENDER__ENABLED=true SFTPGO_COMMON__DEFENDER__DRIVER=provider # 'memory' on single-node, 'provider' for HA SFTPGO_COMMON__DEFENDER__BAN_TIME=60 # ACME (Let's Encrypt) — prefer the WebAdmin UI (Server Manager → Configurations # → ACME). For headless / scripted setups, use file-based config as below and # run `sftpgo acme run --database-protocols 7` once to register and obtain the # first certificate; renewals are then automatic. # --database-protocols bitmask: 1 HTTPS, 2 FTPS, 4 WebDAV (combinable; 7 = all) # key_type: 2048 / 3072 / 4096 / 8192 (RSA) or P256 / P384 (ECDSA) SFTPGO_ACME__EMAIL=ops@example.com SFTPGO_ACME__KEY_TYPE=4096 SFTPGO_ACME__DOMAINS=sftp.example.com SFTPGO_ACME__HTTP01_CHALLENGE__PORT=80 # Telemetry + metrics + profiler (internal only; port 10000 is the convention, # hardcoded by the official Helm chart and used throughout the docs) SFTPGO_TELEMETRY__BIND_ADDRESS=127.0.0.1 SFTPGO_TELEMETRY__BIND_PORT=10000 SFTPGO_TELEMETRY__ENABLE_PROFILER=true SFTPGO_TELEMETRY__AUTH_USER_FILE=/etc/sftpgo/telemetry.htpasswd # HAProxy PROXY protocol SFTPGO_COMMON__PROXY_PROTOCOL=2 # 2 = strict SFTPGO_COMMON__PROXY_ALLOWED=10.0.0.0/8,192.168.0.0/16 # Memory pipes (required on read-only / ephemeral / distroless hosts) SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1 # License key SFTPGO_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX ``` **About memory pipes.** On cloud storage backends (S3 / GCS / Azure Blob), each transfer streams through a per-upload pipe. By default that pipe is file-backed and requires a writable local temp directory. On distroless containers, read-only rootfs, and Kubernetes pods without a writable PVC there is no such directory, and uploads fail with errors like "unable to open pipe" / "read-only file system". Setting `SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1` makes transfers fully in-memory. Higher RAM per concurrent transfer, but no filesystem dependency. Already the default on SFTPGo cloud-marketplace offerings. ### 2.7 Schema init & migrations With the default `data_provider.update_mode = 0`, SFTPGo creates the schema on first startup and migrates on every subsequent startup — no manual step needed beyond creating the empty database (PostgreSQL / MySQL / CockroachDB) and granting DDL to the configured user. Run `sftpgo initprovider` only if you set `update_mode = 1` to disable automatic updates (e.g. runtime credentials have no DDL privileges). CockroachDB does not implement `pg_advisory_lock`, so migrations cannot be serialized at the database level. Make sure only one SFTPGo instance runs migrations against CockroachDB at a time (start instances sequentially, or set `update_mode = 1` everywhere and run `initprovider` from a single operator job). ### 2.8 Reload commands after config change - Linux: `sudo systemctl restart sftpgo` (full restart), `sudo systemctl reload sftpgo` (reload file-based TLS certs via SIGHUP — only needed when cert files on disk are swapped; certs stored in the DB via the WebAdmin UI or ACME reload automatically). - Windows: `Restart-Service sftpgo`, `sftpgo service reload` (paramchange — same scope as above). ### 2.9 Docker — production shape ```shell docker run -d --name sftpgo \ -p 22:22 \ -p 8080:8080 \ -e SFTPGO_SFTPD__BINDINGS__0__PORT=22 \ -e SFTPGO_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX \ -e SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1 \ --mount type=bind,source=/srv/sftpgo-data,target=/srv/sftpgo \ --mount type=bind,source=/srv/sftpgo-config,target=/var/lib/sftpgo \ registry.sftpgo.com/sftpgo/sftpgo: ``` Host dirs must be writable by UID `1000` (`chown -R 1000:1000 /srv/sftpgo-data /srv/sftpgo-config` — no Linux user with UID 1000 needed on the host). For Compose, mount `./env.d:/var/lib/sftpgo/env.d:ro` and drop numbered `.env` files there. Distroless images on cloud-only deployments must set `SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1`. ### 2.10 Kubernetes — shape and SFTPGo-specific decisions Official Helm chart: `oci://ghcr.io/sftpgo/helm-charts/sftpgo`. The critical choices an AI agent must get right: - **Expose SFTP via Service `LoadBalancer` / `NodePort`, not Ingress.** SFTP / FTP / WebDAV are plain TCP — an HTTP/L7 Ingress cannot forward them. The HTTP WebAdmin / WebClient can sit behind an Ingress. Cloud: AWS NLB, Azure TCP LB, GCP TCP LB. NGINX Ingress can forward TCP via the [TCP services ConfigMap](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/). - **Multi-replica needs an external data provider.** SQLite / bolt are single-writer; for `replicaCount > 1` use PostgreSQL / MySQL / CockroachDB (remember: CockroachDB has no `pg_advisory_lock` → serialize migrations or use `update_mode=1`). - **Persistence depends on the backend mix.** Local-home users need a PVC (RWO for single replica, RWX for multi). Cloud-backend-only deployments can run fully ephemeral with memory pipes enabled. - **Memory pipes are mandatory** on ephemeral / read-only pods — `SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1` (preconfigured on EKS / AKS marketplace offerings). - **Health probes** → the chart wires them up automatically against `/healthz` on port `10000` (telemetry, hardcoded by the chart). No manual probe config needed. - **License key + DB password** → Kubernetes `Secret` mounted via `envFrom`, never in committed values.yaml. - **Shared SSH host keys across replicas.** Without this each pod generates its own keys and clients see mismatch warnings. Two patterns: 1. **Paste the keys in the WebAdmin** (Server Manager → Configurations → SFTP) — stored encrypted in the data provider, loaded identically by every replica sharing the DB. Preferred for new deployments. 2. **Mount a `Secret` with the key files** and point SFTPGo at the paths via `SFTPGO_SFTPD__HOST_KEYS` (comma-separated). The chart exposes `volumes:` / `volumeMounts:` / `envVars:` extension points for this: ```yaml volumes: - name: ssh-keys secret: secretName: sftpgo-host-keys optional: false volumeMounts: - name: ssh-keys mountPath: /etc/sftpgo/ssh-keys readOnly: true envVars: - name: SFTPGO_SFTPD__HOST_KEYS value: "/etc/sftpgo/ssh-keys/id_rsa,/etc/sftpgo/ssh-keys/id_ecdsa" ``` ### 2.11 Operational recipes - **Reset a lost admin password**: `sftpgo resetpwd --admin `. Interactive prompt; also disables 2FA on that admin. Not for the memory provider. Stop SFTPGo first when using embedded bolt / SQLite; safe to run live on shared PostgreSQL / MySQL / CockroachDB. On Windows: `"C:\Program Files\SFTPGo\sftpgo.exe" resetpwd --admin -c "C:\ProgramData\SFTPGo Enterprise"`. - **Rotate logs on demand**: `kill -USR1 $(pgrep -x sftpgo)` on Linux; `sftpgo service rotatelogs` on Windows. - **Reload TLS certs after swapping files on disk** (file-based certs only — UI / ACME certs reload automatically): `systemctl reload sftpgo` (Linux) / `sftpgo service reload` (Windows, sends `paramchange`). - **Dump / restore data**: `POST /api/v2/dumpdata` / `POST /api/v2/loaddata` or WebAdmin Maintenance. Portable across DB types and supported upgrade paths. ### 2.12 Reading the logs SFTPGo writes structured JSON, one object per line; each record carries a `sender` field. Locations: | Install | Location | | ----------------------------- | -------------------------------------------------------------------------------------------- | | Debian / RPM systemd | `/srv/sftpgo/logs/sftpgo.log` + `journalctl -u sftpgo` | | Docker / Kubernetes | stdout — `docker logs ` / `kubectl logs ` | | Windows installer service | `C:\ProgramData\SFTPGo Enterprise\logs\sftpgo.log` + Windows Event Log | Useful `sender` values: `login` (successful logins), `connection_failed` (auth failures, client aborts, timeouts — `error` has the reason), `Upload` / `Download`, command senders (`Rename`, `Mkdir`, `Rmdir`, `Remove`, `Chmod`, `Chown`, `Chtimes`, `Copy`, `Truncate`, `SSHCommand`), `httpd` (REST / WebAdmin / WebClient). ```shell # Failed login attempts tail -n 1000 /srv/sftpgo/logs/sftpgo.log \ | jq -c 'select(.sender=="connection_failed") | {time,username,client_ip,protocol,error}' # 5xx REST API responses grep '"sender":"httpd"' /srv/sftpgo/logs/sftpgo.log \ | jq -c 'select(.resp_status >= 500) | {time,method,uri,resp_status,remote_addr}' ``` ### 2.13 Deployment pitfalls - **Avoid setting `common.temp_path`.** Leave empty unless you have a specific reason — a path on a different filesystem from user homes causes atomic renames to fall back to copy+delete. - **Cloud backend upload fails with "unable to open pipe" / "read-only file system" / "no space left"** → SFTPGo cannot create a file-backed pipe in the configured temp dir. Fix: enable memory pipes (`SFTPGO_HOOK__MEMORY_PIPES__ENABLED=1`). Mandatory for distroless / read-only rootfs / pods without a writable PVC; already default on marketplace offerings. - **Local user "permission denied" on upload while login succeeds** → the APT/YUM packages run under a dedicated `sftpgo` system account; a `home_dir` outside `/srv/sftpgo` you created as root is unreadable/unwritable by it. Fix on Linux: `chown -R sftpgo:sftpgo ` and `chmod +x` the parents. On Windows: grant the service account (`LOCAL SYSTEM` by default) write access on the chosen tree via `icacls`. - **`proxy_protocol=2` without populated `proxy_allowed`** → every connection is rejected. Configure both together. - **`allowlist_status=1` before populating the list** → drops everything. - **Changing a tagged-union `driver`/`provider` without its sub-config** → service starts but cannot reach the target (DB, backend, IdP). - **`env.d` does not override process env**. If a value is exported at the systemd / container level, changes under `env.d/` are ignored until the outer source is cleared. - **Missing TLS reload after swapping cert files on disk** (file-based certs only) → in-memory keys stay stale. `systemctl reload sftpgo` / `sftpgo service reload`. UI / ACME-managed certs reload automatically. - **Non-contiguous list-of-struct indices** → a `__0__` + `__2__` without `__1__` leaves index 1 unset. - **Running `initprovider` when not needed** → harmless but unnecessary with `update_mode = 0`. Needed only with `update_mode = 1`. - **CockroachDB + concurrent migrations** → no `pg_advisory_lock`. Serialize instance startup or set `update_mode=1` everywhere and run `initprovider` from a single operator job. - **Windows service account change + privileged ports** → make sure the new account still has the right to bind low ports. - **Windows path escaping in env files** → single quotes are literal (`'C:\path'`); double quotes require backslash escaping (`"C:\\path"`); forward slashes also work. - **Per-replica host keys in K8s** → without explicit sharing each pod generates its own keys and clients see key-mismatch warnings. Store host keys in the data provider via WebAdmin (Server Manager → Configurations → SFTP) or mount a Kubernetes Secret with the private keys. ### 2.14 Fully-managed SaaS at sftpgo.com Separate distribution channel: we own the host, the customer only sees WebAdmin + WebClient + SFTP + REST. **No shell, no config file, no env vars.** Detection signals: host is a dedicated per-instance subdomain we assigned (or a customer-owned subdomain CNAME'd onto one), `bucket=default` + `region=default` in the config, customer mentions "managed service" / "SaaS" / "sftpgo.com plan". When in doubt, ask the user. Preconfigured on every plan: - Integrated S3: sentinel values `bucket=default`, `region=default`; credentials managed server-side. - **Group named `default`**, already created, pre-wired with the integrated storage and key prefix `users/%username%/`. Assign users to it as Primary — storage is inherited. - Simplified user form for the default admin (advanced sections hidden). - Audit logs, Defender, recycle bin (3-day retention) enabled. - **Backup action disabled** — backups are managed by us. For an additional copy, use an Event Manager rule that copies files to storage the customer owns. Tier gates: Small+ for Geo-IP and HIPAA; Standard+ for PGP, LDAP, OIDC; Document editing is an add-on. Configuration changes that trigger a short restart (OIDC, LDAP, first-time Allow List, HIPAA mode): tell the customer to save the config and wait. We are **notified automatically**; in Italy business hours (09:00–20:00 CET / CEST) we verify and restart within a few minutes. No ticket required. Off-hours urgencies: email support. Custom domain: DNS **CNAME** pointing a customer subdomain (e.g. `sftp.example.com`) to the dedicated address we emailed them at activation. A records are discouraged (our IPs can change). Once the CNAME resolves, we provision a Let's Encrypt certificate automatically. What NOT to suggest to a SaaS user: `systemctl` / `Restart-Service` / `docker run` / Helm values / `sftpgo resetpwd` / editing `sftpgo.json` / setting `SFTPGO_…` env vars / the Backup event action. All of §2.1–2.13 above is off-limits on the SaaS. Use WebAdmin forms and REST API only. **Marketplace offerings ≠ SaaS.** AWS / Azure / GCP cloud-marketplace listings are self-hosted — the customer owns the VM or managed K8s namespace. They can edit env files and run CLI commands. Use §2.1–2.13 for them. ### 2.15 Open Source edition and third-party distributions Almost everything in this reference targets the **Enterprise** edition. The **Open Source** edition (`github.com/drakkan/sftpgo`, Docker Hub `drakkan/sftpgo`) has the full protocol stack, every storage backend, the WebAdmin and WebClient, the REST API, basic OIDC, and a basic Event Manager — but it does not have: - The full Go template engine with `toJson` / helpers / conditions / loops (most examples in §3 and in `examples.md` assume this). - Action types: **ICAP**, **IMAP**, **event reports** (type 19), **PGP**, **execute-before-file-publish** staged uploads, enhanced copy with source disposition / glob / retries, data retention with archival. - OIDC role mapping, PKCE-without-secret, session control, Azure B2C, custom labels. - WebClient WOPI / Collabora, TUS resumable uploads. - Share email authentication, group governance, path / scope restrictions. - Cloud backend performance optimizations (memory pipes / small-file speedups), GCS HNS, SFTP backend SOCKS proxy, FTP backend. - Clustering, the WebAdmin UI for LDAP / Geo-IP / email templates / SSH host keys, API key management UI, and the Enterprise builds of the `eventstore` / `eventsearcher` plugins that power the audit-logs browsing UI (open-source builds of those plugins are also available on OSS, and the browsing UI itself works in both editions — what differs is the depth of the captured data). Canonical comparison: the "What Enterprise adds" table on the Enterprise docs landing page (`https://docs.sftpgo.com/enterprise/`, raw: `https://raw.githubusercontent.com/sftpgo/docs/enterprise/docs/index.md`). Distribution channels for the OSS binary include GitHub releases and Docker Hub, plus third-party repackagings by managed-hosting providers, NAS / home-lab app stores, community Linux-package archives, and unofficial Docker images / Kubernetes manifests. **These third-party distributions are not endorsed by the SFTPGo project.** They deliver the OSS feature set; their own support channels are separate; bug reports about the upstream binary belong on `github.com/drakkan/sftpgo`. Detection signals for OSS: user installed from `github.com/drakkan/sftpgo`, image is `drakkan/sftpgo:...`, install came from any of the third-party wrapper types above, "free version" / "no license", the cloud-marketplace listing is explicitly labelled "SFTPGo Open Source". Docs for OSS live at `https://docs.sftpgo.com/latest/` (raw: `https://raw.githubusercontent.com/sftpgo/docs/main/docs/.md`). When answering an OSS user: - Lead with OSS-compatible suggestions when they exist (basic HTTP action without `toJson` still works for simple webhooks). - When the feature is Enterprise-only, state it once, neutrally — *"this is an Enterprise-only feature; on OSS the closest equivalent is X, or there is none"* — and stop. No repeated upgrade pitches. - Do not invent OSS workarounds for things that genuinely require Enterprise (ICAP, PGP, event reports, WOPI, clustering, email-authenticated shares, LDAP, most of the UI-based configuration pages). - When uncertain whether the user is on OSS or Enterprise, ask. --- ## 3. Event Action templates ### 3.1 What these templates are SFTPGo Event Actions execute in response to triggers (filesystem events, provider events, cron schedules, IDP logins, IP blocks, certificate renewals, on-demand runs). Several fields are rendered as **Go `text/template`** strings against a shared context: email body/subject, HTTP body/headers, command args, IDP user/admin JSON, ICAP and filesystem paths, IMAP subject/body. - Syntax: Go `text/template` — `{{.Field}}`, `{{if}}{{end}}`, `{{range}}{{end}}`, `{{with}}{{end}}`, pipes `{{.X | toJson}}`. - **Dot prefix mandatory** for field access: `{{.Name}}`, not `{{Name}}`. - A custom function map extends the built-ins (§3.3). - Template parse errors reject the save. A field that is not populated for the chosen trigger renders silently as the empty string / zero value. ### 3.2 Template context Every template sees the following keys. Some are populated only for certain triggers — see §3.5. | Key | Type | Summary | | -------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.Name` | `string` | Event subject username (uploader, affected entity). | | `.ExtName` | `string` | External username from IDP when it differs from the mapped local one. | | `.Event` | `string` | Event name — see §3.5 for per-trigger values. | | `.Status` | `int` | `1` success, `2` generic error, `3` quota exceeded (fs events). | | `.VirtualPath` | `string` | Virtual path (fs events). | | `.FsPath` | `string` | Filesystem path (fs events). | | `.VirtualTargetPath`, `.FsTargetPath` | `string` | Destination paths (rename / copy only). | | `.ObjectName` | `string` | File basename (fs) or primary key of the object (provider). | | `.ObjectType` | `string` | E.g. `user`, `folder`, `group`, `share`, `admin`, `api_key`, `event_rule`, `event_action` for provider events. | | `.FileSize` | `int64` | Bytes transferred (0 on `pre-*` and non-fs). | | `.Elapsed` | `int64` | Milliseconds (0 on `pre-*` and non-fs). | | `.Protocol` | `string` | `SFTP`, `SCP`, `SSH`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`. | | `.IP` | `string` | Client IP. | | `.Role` | `string` or array | For IDP login: populated from the OIDC binding's `role_field` claim. **Not available via `.IDPFields` unless separately added to `custom_fields`.** For other events: the user's role from the data provider. | | `.Email` | `string` | Subject's email. For IDP login, populated from the `email` claim only if `email` is in `custom_fields`. | | `.Timestamp` | `time.Time` | Event time. Call methods: `.UTC`, `.Format`, `.Unix`, `.UnixNano`. | | `.UID` | `string` | Unique event id (xid). | | `.Metadata` | `map[string]string` | Custom fs metadata. | | `.IDPFields` | `map[string]any` | **Only the OIDC claims listed in the binding's `custom_fields` config.** Access with `index` or `.IDPFields.key`. | | `.Errors` | `[]string` | Error messages when `.Status != 1`. | | `.Object` | lazy | Trigger object for provider events. Accessors: `.Object.JSON`, `.Object.User`, `.Object.Admin`, `.Object.Group`, `.Object.Share`. | | `.Initiator` | lazy, never nil | Who caused the event. Accessors: `.Initiator.User` (`*User` or nil), `.Initiator.Admin` (`*Admin` or nil), `.Initiator.JSON`. | | `.Shares` | lazy | Shares owned by `.Name`. Call `.Shares.Load` to get `[]Share`. | | `.RetentionChecks` | array of retention check records | Populated after a Data Retention action has run earlier in the chain. | | `.EventReports` | array of per-user event report records | Populated after an Event Report action. | | `.ShareExpirationChecks`, `.ShareExpirationResult` | array / single | Populated after a Share Expiration Check action. | **`.Object` typed accessors** - `.Object.User` → `*User` (or nil). Fields include `.Username`, `.Email`, `.Status`, `.Role`, `.HomeDir`, `.UID`, `.GID`, `.MaxSessions`, `.QuotaSize`, `.QuotaFiles`, `.ExpirationDate`, `.LastLogin`, `.Permissions` (`map[string][]string`, e.g. `{{index .Object.User.Permissions "/"}}`), `.Filters`, `.Description`, `.AdditionalInfo`. Confidential data (password, public keys, TOTP secrets) is stripped. - `.Object.Admin` → `*Admin`. Fields: `.Username`, `.Email`, `.Status`, `.Role`, `.Permissions`, `.Description`, `.AdditionalInfo`, `.Groups`. Confidential data stripped. - `.Object.Group` → `*Group`. Fields: `.Name`, `.Description`, `.UserSettings`, `.VirtualFolders`. - `.Object.Share` → `*Share`. Fields: `.ShareID`, `.Name`, `.Description`, `.Scope`, `.Paths`, `.Username`, `.CreatedAt`, `.UpdatedAt`, `.LastUseAt`, `.ExpiresAt`, `.UsedTokens`, `.MaxTokens`, `.AllowFrom`. - `.Object.JSON` → the object's full JSON. **`.Initiator` accessors** `.Initiator.User` / `.Initiator.Admin` return `*User` / `*Admin` or `nil`. Same field surface as `.Object.User` / `.Object.Admin`. `.Initiator.JSON` returns `{}` for no-initiator events (cron, IP blocked, etc.). **`.RetentionChecks[i]` fields** `.Username`, `.Email` (`[]string`), `.ActionName`, `.Type` (`0` delete / `1` archive), `.Results`. Each `.Results[j]`: `.Path`, `.Retention` (hours, `int`), `.DeletedFiles` (`int`), `.DeletedSize` (`int64`), `.Elapsed` (`time.Duration`), `.Info`, `.Error`. **`.EventReports[i]` fields** `.Username`, `.Email` (`[]string`), `.Truncated` (`bool`), `.Events` (array). Each `.Events[j]`: `.ID`, `.Timestamp` (int64 nanos; use `fromNanos`), `.Action`, `.Username`, `.ExternalUsername`, `.VirtualPath`, `.VirtualTargetPath`, `.FileSize`, `.Elapsed`, `.Status`, `.Protocol`, `.IP`, `.SSHCmd`. ### 3.3 Template functions In addition to standard `text/template` built-ins (`printf`, `len`, `index`, `slice`, `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `and`, `or`, `not`, `call`, `html`, `js`, `urlquery`), the following are registered: **JSON / encoding** - `toJson(val) string` — JSON-encode, quoting strings. - `toJsonUnquoted(val) string` — for strings, JSON-escape without outer quotes; for others, same as `toJson`. Use inside pre-quoted JSON fields. - `toBase64(s) string`, `toHex(s) string`. **URL** - `urlEscape(s) string` (= `url.QueryEscape`), `urlPathEscape(s) string` (= `url.PathEscape`). **Path** - `pathDir(p) string`, `pathBase(p) string`, `pathExt(p) string`. - `pathJoin(elems []string) string` — slash-join, cleaned. - `filePathJoin(elems []string) string` — OS-specific. **String** - `stringSlice(args ...string) []string`. - `stringJoin(sep string, strs []string) string` — **separator first**. - `stringTrimSuffix(s, suffix) string`, `stringTrimPrefix(s, prefix) string`. - `stringReplace(old, new, s) string` — **source last** (not like `strings.ReplaceAll`). - `stringHasPrefix`, `stringHasSuffix`, `stringContains` — `(s, sub) bool`. - `stringToLower`, `stringToUpper` — `(s) string`. **Slice / map** - `slicesContains(slice []any, val any) bool`. - `mapToString(key any, m map[any]string) string`. - `createDict(entries ...any) map[any]string` — alternating key/value pairs. **Time / bytes** - `humanizeBytes(int64) string` — IEC units. - `fromMillis(msec int64) time.Time`, `fromNanos(nsec int64) time.Time`. `.Timestamp` is already `time.Time`; use `fromMillis` / `fromNanos` for nested numeric timestamps inside `.EventReports[i].Events[j].Timestamp`. ### 3.4 Action types (enum → sub-config) The action's integer `type` field selects which sub-configuration is read from `options`. Only the sub-configuration matching `type` is active; all others are zeroed on save. | `type` | Name | `options.*` field | Template fields | | ------ | ------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | 1 | HTTP | `http_config` | `endpoint`, `body`, `query_parameters[].value`, `headers[].value`, `parts[].filepath`, `parts[].body`, `parts[].headers[].value` | | 2 | Command | `cmd_config` | `args[]`, `env_vars[].value` | | 3 | Email | `email_config` | `recipients[]`, `bcc[]`, `subject`, `body`, `attachments[]` | | 4 | Backup | (none) | — | | 5–7 | Quota resets | (none) | — | | 8 | Data retention check | `retention_config` | (none) | | 9 | Filesystem | `fs_config` | Depends on `fs_config.type` — see below | | 11 | Password expiration check | `pwd_expiration_config` | (none) | | 12 | User expiration check | (none) | — | | 13 | IDP account check | `idp_config` | `template_user`, `template_admin` | | 14 | User inactivity check | `user_inactivity_config` | (none) | | 15 | Rotate logs | (none) | — | | 16 | IMAP | `imap_config` | `email[]`, `subject`, `body`, `attachments[]`, `target_folder`, `flags[]` | | 17 | ICAP | `icap_config` | `paths[]` | | 18 | Share expiration check | `share_expiration_config` | (none) | | 19 | Event report | `event_report_config` | (none — results surface via `.EventReports`) | (`10` is reserved.) **`fs_config.type` values (for `type=9`)** | `fs_config.type` | Name | Config field(s) | Template-rendered | | ---------------- | -------------- | ---------------------------------------- | -------------------------------------------------------- | | 1 | Rename | `renames[]` (`{key: old, value: new}`) | both per entry | | 2 | Delete | `deletes[]` (`[]string`) | each entry; **trailing `/` = delete contents, keep dir** | | 3 | Mkdirs | `mkdirs[]` | each | | 4 | Exist | `exist[]` | each | | 5 | Compress | `compress.paths[]`, `compress.name` | all | | 6 | Copy | `copy[]` (`{key, value}`) | both per entry | | 7 | PGP | `pgp.paths[]` + key config | paths | | 8 | Metadata check | `folders[]` | none (literal names) | | 9 | Decompress | `decompress.source`, `decompress.target` | both | **`idp_config` (type 13)** ```json { "mode": 0, // 0 = create/update, 1 = create only "template_user": "…", // Go template rendering JSON User "template_admin": "…" // Go template rendering JSON Admin } ``` At least one of `template_user` / `template_admin` must be set. Licensing: **Premium tier only**. Other Premium-tier-only action types: `13` (IDP), `17` (ICAP), `19` (Event Report). Type `16` (IMAP) is available on Starter as well. ### 3.5 Trigger types (what populates the context) | `trigger` | Name | Populates | Typical use | | --------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | | 1 | Filesystem event | `.Name`, `.Event`, `.Status`, `.VirtualPath`, `.FsPath`, `.VirtualTargetPath`, `.FsTargetPath`, `.ObjectName`, `.ObjectType`, `.FileSize`, `.Elapsed`, `.Protocol`, `.IP`, `.Timestamp`, `.UID`, `.Metadata`, `.Errors`, `.Initiator.User` | Upload webhook, ICAP scan, compress on upload. | | 2 | Provider event | `.Name`, `.Event` (`add` / `update` / `delete`), `.ObjectName`, `.ObjectType`, `.Object`, `.Timestamp`, `.UID`, `.Initiator` | Audit, CRM sync on user add. | | 3 | Schedule (cron) | `.Timestamp`, `.UID`. Chain-populated fields (e.g. `.RetentionChecks`) come from earlier actions. | Nightly retention report. | | 4 | IP blocked | `.IP`, `.Protocol`, `.Event`, `.Timestamp`, `.UID`, `.Errors` | SOC alert. | | 5 | Certificate renewal | `.Event`, `.Timestamp`, `.UID`, `.Errors` | Ops alert on ACME. | | 6 | On demand | Caller-injected | Manual triggers. | | 7 | IDP login | `.Name`, `.ExtName`, `.Role`, `.Email`, `.Protocol = "OIDC"`, `.IP`, `.Timestamp`, `.UID`, `.IDPFields` | JIT user / admin provisioning. | `fs_events` sub-filter (trigger 1): `upload`, `pre-upload`, `first-upload`, `download`, `pre-download`, `first-download`, `delete`, `pre-delete`, `rename`, `mkdir`, `rmdir`, `copy`, `ssh_cmd`. `provider_events` sub-filter (trigger 2): `add`, `update`, `delete`. `provider_objects` sub-filter: `user`, `folder`, `group`, `admin`, `api_key`, `share`, `event_rule`, `event_action`. `idp_login_event` sub-filter (trigger 7): `0` any, `1` user, `2` admin. A template that references a field the trigger does not populate **renders an empty string**, not an error. ### 3.6 Template pitfalls 1. **Missing dot prefix** — `{{Name}}` is invalid; use `{{.Name}}`. Old snippets that omit the dot are wrong. 2. **`.IDPFields` does not contain the role claim.** Unless the OIDC binding's `custom_fields` includes the role claim (e.g. `sftpgo_role`), `index .IDPFields "sftpgo_role"` returns `nil` forever. The role is in `.Role`, populated from the binding's separate `role_field` setting. This is the #1 OIDC template bug. 3. **Role may be a string or an array.** When `role_field` points at a JSON-array claim (e.g. Entra ID `groups`), `.Role` is an array. Use `slicesContains .Role "value"` instead of `eq .Role "value"`. 4. **Always guard `.Initiator.User` / `.Initiator.Admin`.** `.Initiator` is never nil, but exactly one of `.User` / `.Admin` is nil. Guard with `{{if .Initiator.User}}…{{end}}`. 5. **`.Object` is a lazy wrapper, not the object.** Use `{{.Object.User.Username}}` or `{{.Object.JSON}}` — not `{{.Object.Username}}`. 6. **JSON embedding.** Inside `"…"` positions: `{{.X | toJsonUnquoted}}`. In unquoted positions: `{{toJson .X}}`. Never hand-roll quoting. 7. **`stringReplace` order.** `stringReplace old new s` — source is last, opposite of `strings.ReplaceAll`. 8. **`pathJoin` takes a `[]string`.** Build with `stringSlice`: `{{pathJoin (stringSlice "/base" .Name)}}`. 9. **Trailing-slash delete.** `fs_config.type=2` with a path ending in `/` empties the directory but keeps it; without `/` the directory is removed. Running the path through `pathJoin` strips the slash. 10. **`.FileSize` / `.Elapsed` are 0 on `pre-*` events.** Condition on `.Event` if a size threshold matters. 11. **Schedule triggers don't have `.VirtualPath`.** Cron-triggered rules see only `.Timestamp` + chained data. Restructure, don't fight it. 12. **"Compiles but webhook body is empty"** is almost always a trigger/field mismatch — cross-check every dotted field against the trigger's "Populates" list in §3.5. --- ## 4. Workflow for AI agents When asked to produce SFTPGo artefacts: 1. **Check the edition and distribution first.** If the user is on the Open Source edition or a third-party redistribution (see §2.15 — GitHub `drakkan/sftpgo`, Docker Hub, managed-hosting providers, NAS / home-lab app stores, community Linux-package archives, etc.), most of this reference's Event Manager, OIDC, sharing, and UI-configuration content is Enterprise-only and does not apply. If on the fully-managed SaaS (see §2.14), no CLI / env-var / deployment recipes apply. In both cases, if detection is ambiguous, ask the user. 2. **Classify the question.** Is it a REST API payload, a WebAdmin configuration, a deployment/env-var task, or an Event Action template? (A single question can span more than one — e.g. "set up OIDC JIT" involves §1 + §2 + §3.) 3. **For REST API payloads** start from §1. If a specific field name or enum value is in doubt, look up the OpenAPI spec. Pay special attention to §1.2 (tagged unions) and §1.3 (secret envelope). 4. **For WebAdmin forms**: same shape as §1 JSON; paste the template string directly where the form accepts a template, paste scalar fields raw. 5. **For deployment questions** start from §2. The env-var rule is exact — lists of structs (§2.4) are the most common source of silently-ignored vars. Prefer `env.d/` over editing the shipped config file. Skip this entirely on SaaS — see §2.14. 6. **For templates**: a. Identify the trigger (`trigger` integer) from §3.5. b. Identify the action type (`type` integer) from §3.4. c. Pick fields from §3.2 that are populated for that trigger. d. Apply §3.6 guards systematically: dot prefix, nil-safe accessors, JSON-safe encoding. e. Produce two forms when unsure: raw template (WebAdmin paste) and full REST API JSON payload. Label them clearly. 7. **Cross-check** every referenced field against §3.2 / §3.5 before returning an answer. A field that is not in the trigger's populated set is a bug. 8. **When the question is operational / how-to** (e.g. "how do I enable 2FA", "how do I set up the LDAP plugin", "how do I configure S3 authentication"), fetch the corresponding page from the public docs (§0) — they contain the operational walkthroughs this reference does not reproduce. --- **Reference version:** v0.5.0 · verified against SFTPGo v2.7.x · upstream