Dittofeed v0.24.0-alpha.21 配置参数详解

Dittofeed 的配置分为三层:环境变量(进程级)、工作区数据库记录(SMTP/SMS 提供商、密钥、写键)、以及构建期常量。本文以 Ubuntu 22.04 源码部署实例(PostgreSQL 15、ClickHouse 24.12.6.70、Temporal 1.22.4、Node.js v20.20.2)为基准,逐项给出默认值、是否必填、生效方式与

DittofeedDittofeed营销自动化Node.jsClickHouse2026/10/27

Dittofeed v0.24.0-alpha.21 配置参数详解

Dittofeed 的配置分为三层:环境变量(进程级)、工作区数据库记录(SMTP/SMS 提供商、密钥、写键)、以及构建期常量。本文以 Ubuntu 22.04 源码部署实例(PostgreSQL 15、ClickHouse 24.12.6.70、Temporal 1.22.4、Node.js v20.20.2)为基准,逐项给出默认值、是否必填、生效方式与验证命令,所有「实测」条目均来自该实例的真实回显。

安装步骤见 《Ubuntu 22.04 安装 Dittofeed v0.24.0-alpha.21 指南》,产品定位与版本选型见 《Dittofeed v0.24.0-alpha.21 介绍与选型指南》。本文只讨论参数本身。

一、配置加载机制

1.1 加载顺序

packages/backend-lib/src/config/loader.ts 的 loadConfig() 先用 dotenv.config({ path: <baseDir>/.env }) 读取 .env,再从 process.env 取值。dotenv 默认不覆盖已存在的环境变量,因此优先级为:

命令行/服务单元 Environment= 赋的值  >  .env 文件  >  代码内置默认值

baseDir 由 findBaseDir()(packages/backend-lib/src/dir.ts)决定:从进程工作目录向上找名为 packages 的目录,取其父目录。所以三个进程的 WorkingDirectory 必须是 packages/api、packages/worker、packages/dashboard,.env 才会被读到;若从 /srv/dittofeed 直接启动,则 baseDir=/srv/dittofeed,同样成立。

1.2 变量命名规则

配置对象字段经 change-case 的 constantCase() 转成环境变量名:

databaseHost      -> DATABASE_HOST
clickhouseDatabase-> CLICKHOUSE_DATABASE
temporalAddress   -> TEMPORAL_ADDRESS
apiBodyLimit      -> API_BODY_LIMIT
bootstrapEvents   -> BOOTSTRAP_EVENTS

因此不存在 PG_HOST 这类别名,写错名字会被当作未设置而回退默认值,且不报错。

1.3 默认值来源

DEFAULT_BACKEND_CONFIG(packages/backend-lib/src/config.ts)内置了四个兜底值:

export const DEFAULT_BACKEND_CONFIG = {
  databasePassword: "password",
  clickhousePassword: "password",
  password: "password",
  secretKey: "o/UopmFUqiYriIzOCXnzZXGbcYTWuE3iVx2822jC0fY=",
} as const;

这四项在生产部署中必须全部显式覆盖,否则等于使用公开口令与公开签名密钥。

1.4 查看最终生效配置

设置 LOG_CONFIG=true 后,api 与 worker 启动时会打印一份完整的解析后配置(msg":"Initialized with config")。这是排查「变量到底有没有生效」最快的手段:

curl -s http://127.0.0.1:3101/api/ -o /dev/null
grep -o '"databaseName":"[^"]*"' /var/log/dittofeed-api.log | tail -1

实测:LOG_CONFIG=true 时日志原文包含明文口令字段,例如 "databasePassword":"Dit2feedPg2026"、"clickhousePassword":"Dit2feedCh2026";LOG_CONFIG 未设置(默认 false)时,同一次启动只有 {"level":30,"msg":"Server listening at http://0.0.0.0:3101"},没有配置 JSON。生产环境不建议长期开启。

二、PostgreSQL 关系库配置

2.1 参数表

变量默认值必填说明
DATABASE_URL无与分组二选一单条连接串,优先级最高;给了它就不再读下面的分组变量(DATABASE_NAME_SUFFIX 除外)
DATABASE_USERpostgres生产必填生产环境缺少它或口令会抛错
DATABASE_PASSWORDpassword生产必填同上
DATABASE_HOSTlocalhost是数据库地址
DATABASE_PORT5432否端口
DATABASE_NAMEdittofeed(测试环境为 dittofeed_test)否业务库名
DATABASE_NAME_SUFFIX无否追加到默认库名后,- 会被替换为 _
DATABASE_PARAMSconnect_timeout=60否形如 sslmode=require&connect_timeout=30,与默认值合并

DATABASE_URL 拼装逻辑见 parseDatabaseUrl():URL 会强制补上 connect_timeout=60,URL 中的 query 参数优先覆盖默认值。因此 postgresql://postgres:xxx@10.89.59.3:5432/dittofeed_src 与分组写法等价。

生产环境校验在前:

In production must provide database credentials

NODE_ENV=production 且 DATABASE_USER/DATABASE_PASSWORD 任一缺失即启动失败,不会悄悄用 password。

2.2 验证方法

变量是否生效,看库名与表是否落在预期库:

psql "postgresql://postgres:Dit2feedPg2026@10.89.59.3:5432/dittofeed_src" -c '\dt'

实测项 1(DATABASE_NAME):DATABASE_NAME=dittofeed_src 时,information_schema.tables 在当前 schema 下共 31 张表,核心表为引号大写的 "Workspace"、"Secret"、"WriteKey"、"UserProperty";SELECT name, type FROM "Workspace" 返回:

Default|Root
Tenant2|Parent

2.3 与其他进程的关系

api、worker、dashboard 读同一份配置;admin-cli 也读同一份 .env,因此迁移、建工作区、导出配置(admin config-print)与运行时看到的是同一套连接参数。

三、ClickHouse 配置

3.1 参数表

变量默认值必填说明
CLICKHOUSE_HOSThttp://localhost:8123是可写 10.89.59.2(自动补 http:// 与 8123)或带协议完整地址
CLICKHOUSE_USERdittofeed是生产必须显式设置
CLICKHOUSE_PASSWORDpassword是生产必须显式设置
CLICKHOUSE_DATABASEdittofeed(测试环境 dittofeed_test)否事件库名
CLICKHOUSE_PROTOCOLhttp否仅在 CLICKHOUSE_HOST 未写协议时参与补全
CLICKHOUSE_MAX_EXECUTION_TIME300否单查询最大执行秒数
CLICKHOUSE_MAX_MEMORY_USAGE10000000000否单查询内存上限(字节)
READ_QUERY_PAGE_SIZE1000否用户列表分页大小
READ_QUERY_CONCURRENCY10否并发查询数

CLICKHOUSE_HOST 补全规则来自 defaultChUrl():无协议补 http://,无端口补 8123,末尾统一带 /。实测日志中 "clickhouseHost":"http://10.89.59.2:8123/" 即补全结果。

3.2 验证方法

clickhouse client --host 10.89.59.2 --port 9000 \
  --user dittofeed --password Dit2feedCh2026 \
  --query "SELECT name FROM system.databases ORDER BY name"

实测项 2(CLICKHOUSE_DATABASE):同一实例上同时存在 dittofeed(默认库,由部署实例里另一套运行环境创建)和 dittofeed_src(本实例通过 CLICKHOUSE_DATABASE 指定)。查询结果为:

INFORMATION_SCHEMA
default
dittofeed
dittofeed_src
information_schema
system

SHOW TABLES FROM dittofeed_src 返回 25 张表,含 user_events_v2、computed_property_state_v3、user_property_idx_str、user_property_idx_str_mv 等;事件确实写入了配置指定的库:

SELECT event, user_id, count() AS c
FROM dittofeed_src.user_events_v2
GROUP BY event, user_id
ORDER BY c DESC, event
	u-c1-001	1
async_probe	u-async-001	1
doc_test_track	u-c1-001	1
sync_probe	u-sync-001	1

首行为 identify 事件的空 event 字段,说明 identify 走同一条写入链路。

注意:user_events_v2 的列名是 user_id,不是 userId;用 userId 查询会报 Unknown expression identifier 'userId' ... Maybe you meant: ['user_id']。

四、Temporal 配置

变量默认值必填说明
TEMPORAL_ADDRESSlocalhost:7233worker 必填只写主机名时自动补 :7233
TEMPORAL_NAMESPACEdefault否命名空间
TEMPORAL_CONNECTION_TIMEOUT无(使用 SDK 默认)否连接超时毫秒数
GLOBAL_CRON_TASK_QUEUEdefault否全局定时任务队列
COMPUTED_PROPERTIES_TASK_QUEUEdefault否计算属性任务队列
BOOTSTRAP_WORKER生产环境 true否启动时注册默认 workflow/cron

验证方法

worker 启动日志出现 "msg":"Worker created" 与 "state":"RUNNING" 即连上;连接错误会直接抛出。实测项 3(TEMPORAL_ADDRESS 指向错误端口 127.0.0.1:7299):

TransportError: tonic::transport::Error(Transport, ConnectError(ConnectError("tcp connect error", 127.0.0.1:7299, Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })))
    at NativeConnection.connect (/srv/dittofeed/node_modules/@temporalio/worker/lib/connection.js:199:23)

worker 进程随即退出,不会降级为「无 Temporal 继续跑」。api 进程则不同:Temporal 不可用时 api 仍能启动并响应 HTTP,只是触发 journey、计算属性等异步动作失败,因此生产上必须把 worker 与 Temporal 的存活一起监控。

五、HTTP 服务配置

变量默认值必填说明
API_PORT3001否api 监听端口
API_HOST生产 0.0.0.0,开发 localhost否监听地址
API_BODY_LIMIT5242880(5 MB)否请求体上限(字节)
API_SERVICE_NAMEdittofeed-api否服务标识,随日志输出
APP_VERSION无否健康检查返回的版本字段
DASHBOARD_URL生产 https://app.dittofeed.com,开发 http://localhost:3000是控制台基地址,同时用于 ALLOWED_ORIGINS 默认值与退出跳转
DASHBOARD_API_BASE由 DASHBOARD_URL 推导否控制台访问 api 的地址

验证方法

curl -s -i http://127.0.0.1:3101/api/

实测项 4(API_PORT/API_HOST):API_PORT=3101 时日志为 {"msg":"Server listening at http://0.0.0.0:3101"},GET /api/ 返回 200;此时 3001 端口无监听。

实测项 5(APP_VERSION):未设置时响应体是 {}(只有 version 字段且值为 undefined,序列化后消失);设置 APP_VERSION=0.24.0-alpha.21 后重启,同一请求返回:

{"version":"0.24.0-alpha.21"}

实测项 6(API_BODY_LIMIT):默认 5 MB 下发送 6000064 字节的 track 请求:

{"statusCode":413,"code":"FST_ERR_CTP_BODY_TOO_LARGE","error":"Payload Too Large","message":"Request body is too large"}

关于 ALLOWED_ORIGINS

ALLOWED_ORIGINS 虽在配置 schema 中(默认取 DASHBOARD_URL,逗号分隔),但本版本的 api 在 packages/api/src/buildApp/cors.ts 里写死了:

await fastify.register(fastifyCors, {
  origin: "*",
  methods: "*",
  allowedHeaders: "*",
  exposedHeaders: ["Content-Disposition"],
});

实测项 7:无论请求 Origin 是 http://192.168.146.131:8138 还是 http://evil.example.com,响应头都是 access-control-allow-origin: *,OPTIONS 预检返回 204 且 access-control-allow-methods: *。即该变量在本版本中不参与 api 的 CORS 判定,不能依赖它做来源白名单;若需限制来源,必须在反向代理层实现。

六、安全与鉴权配置

变量默认值必填说明
SECRET_KEY固定常量 o/UopmFUqiYriIzOCXnzZXGbcYTWuE3iVx2822jC0fY=是会话 Cookie 签名密钥,32 字节随机串的 base64(44 字符)
PASSWORDpassword单租户必填单租户模式共享登录口令(无用户名)
AUTH_MODEanonymous是anonymous / single-tenant / multi-tenant
SESSION_COOKIE_SECUREfalse生产建议 trueCookie 是否带 Secure
AUTH_PROVIDER无多租户必填多租户控制台登录方式,缺失时会话鉴权 500
SIGN_OUT_URL / SIGN_OUT_REDIRECT_URLsingle-tenant 时为 /api/public/single-tenant/signout;跳转默认 DASHBOARD_URL否退出路径
ALLOWED_ORIGINSDASHBOARD_URL否见 5.3,本版本未生效

生成密钥:

openssl rand -base64 32

6.1 SECRET_KEY

会话由签名 Cookie 承载,密钥变更会让所有旧 Cookie 立即失效。

实测项 8:用旧实例签发的 Cookie 访问用新 SECRET_KEY 启动的实例(新值 Slq3ThXlbdYhKqdE3NMB6jDCEquBvgQ1LgyfumcaiDE=):

POST /api/users  code=401

同一实例用新密钥重新登录后 POST /api/users 返回 200。多副本部署必须共享同一 SECRET_KEY,否则用户会在实例间随机掉线。

6.2 AUTH_MODE

single-tenant:POST /api/public/single-tenant/login 注册,口令正确返回 200 并下发会话 Cookie:

curl -s -i -c /srv/ck.txt -X POST http://127.0.0.1:3101/api/public/single-tenant/login \
  -H 'content-type: application/json' -d '{"password":"q1w2e3r4"}'
HTTP/1.1 200 OK
set-cookie: session=...; Max-Age=1209600; Path=/; HttpOnly

口令错误返回 {"error":"Invalid password."} 与 401。

anonymous:该登录路由不注册。实测项 9(AUTH_MODE=anonymous 单独启动一个实例):

POST /api/public/single-tenant/login  -> 404 {"message":"Route POST:/api/public/single-tenant/login not found",...}
POST /api/users(不带 Cookie)        -> 200

也就是说 anonymous 模式下控制台 API 组完全不校验会话,不能对外暴露,只能用于本地调试或前置了可信网关的场景。

multi-tenant:管理接口必须带 workspaceId(body、query 或请求头 x-workspace-id),缺失时请求体校验直接返回 400 body must have required property 'workspaceId';单个工作区内的管理密钥只对所属工作区及子工作区有效。实测项 10:多租户模式下走会话鉴权的 /api/users 返回:

{"statusCode":500,"error":"Internal Server Error","message":"Misconfigured auth provider, missing."}

即 multi-tenant 必须同时配置 AUTH_PROVIDER 才能使用控制台。

6.3 PASSWORD

单租户模式只校验 PASSWORD,没有用户名,示例口令统一为 q1w2e3r4。

实测项 11(PASSWORD 被显式置空):

{"error":"Application is misconfigured, contact support."}  code=500

代码路径是 if (!password?.length) { 500 },所以空口令不会退化成默认口令。但完全不设置该变量时,rawConfig.password ?? DEFAULT_BACKEND_CONFIG.password 会落到字符串 password,即控制台口令就是 password,这是必须避免的部署姿势。

默认 false,登录响应只有 HttpOnly;置为 true 后:

set-cookie: session=...; Max-Age=1209600; Path=/; HttpOnly; Secure

实测项 12。上线 HTTPS 后应同时开启该开关,否则 Cookie 会在明文 HTTP 上传输。

七、日志配置

变量默认值必填说明
LOG_LEVEL生产 info、开发 debug、测试 error否pino 日志级别
LOG_CONFIGfalse否启动时打印完整配置(含口令)
PRETTY_LOGS生产 false、开发/测试 true否人类可读格式,生产建议关闭
GOOGLE_OPSfalse否是否启用 GCP 日志字段
START_OTEL / OTEL_COLLECTORfalse / http://localhost:4317否OpenTelemetry 导出

实测项 13(LOG_LEVEL=error):以该级别启动另一个实例,发起一次 GET /api/ 后日志文件仅 2031 字节,且 grep -c 'incoming request' 为 0;默认 info 级别下同一请求会产生成对的 "msg":"incoming request" 与 "msg":"request completed"。因此排障时临时降到 debug,压测时升到 warn/error 可显著降低日志量。

八、初始化与写入开关

变量默认值必填说明
BOOTSTRAPfalse否仅被 lite 入口和 admin-cli 消费
BOOTSTRAP_EVENTSfalse否初始化时是否写入默认事件
BOOTSTRAP_WORKER生产 true否worker 启动时注册默认 workflow 与 cron
BOOTSTRAP_SAFEfalse否安全模式初始化(不覆盖已有数据)
WRITE_MODEch-async(测试环境 ch-sync)否事件写入策略
DEFAULT_USER_EVENTS_TABLE_VERSION空否已废弃字段,保留兼容

8.1 BOOTSTRAP 的真实作用范围

实测项 14:以 BOOTSTRAP=true 启动 api 后请求用户列表:

relation "workspace" does not exist

api 与 worker 都只把该值原样打进启动日志("bootstrap":true),并不执行迁移。真正消费它的是 packages/lite/scripts/startLite.ts 与 packages/admin-cli:源码安装必须先执行

node /srv/dittofeed/packages/admin-cli/dist/scripts/cli.js bootstrap -n Default

日志出现 "msg":"Running migrations" 与 "msg":"Upserting workspace." 后,表结构才建立。仓库自带的 scripts/bootstrap.sh 调用 node ./packages/api/dist/scripts/bootstrap.js,该产物在本版本不存在,属陈旧脚本,不要用。

8.2 WRITE_MODE

ch-async 为生产默认:事件先进入应用侧缓冲,再由后台批量写入 ClickHouse;ch-sync 在请求内同步写入。

实测项 15:分别在 ch-sync 与 ch-async 实例上各发一条 track 后立即查询:

ch-sync : track=204,79 ms 后查询到 1 条(sync_probe)
ch-async: track=204,141 ms 后查询到 1 条(async_probe)

小流量下两种模式的「写入后可见」延迟都在百毫秒级,差异主要出现在批量高峰;ch-sync 会把 ClickHouse 抖动直接传导为接口延迟,生产不建议切换,除非下游强依赖写后立即可查。

九、邮件、短信与工作区级配置(不在 .env 中)

SMTP、SendGrid、Postmark、Amazon SES、MailChimp、Resend、Twilio、SignalWire 等提供商参数不是环境变量,而是按工作区存储在 PostgreSQL 的 "Secret" 表(name 为 smtp、sendgrid 等,configValue 为 JSON)。

配置入口(会话鉴权):

curl -s -X PUT http://127.0.0.1:3101/api/settings/email-providers \
  -b /srv/ck.txt -H 'content-type: application/json' \
  -d '{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","setDefault":true,
       "config":{"type":"Smtp","host":"smtp.example.com","port":"587",
                 "username":"mailer","password":"SmtpPass2026"}}'

实测项 16:返回 201;库中记录为

Default | smtp | {"host": "smtp.example.com", "port": "587", "type": "Smtp", "password": "SmtpPass2026", "username": "mailer"}

用 GET /api/secrets?workspaceId=...&names=smtp 查询却返回 []:因为列表接口只返回 value 非空的行,SMTP 参数在 configValue 中,不会通过 HTTP 回显,属于有意的防泄漏设计。校验请直接查库或看控制台设置页。

GET /api/secrets?workspaceId=... 不带 names 时,本实例只返回 default-write-key 与 subscription-key 两个名称,可作为「密钥是否初始化」的快速探针。

十、工作区与多租户

工作区没有 HTTP CRUD 接口(GET /api/admin/workspaces 实测返回 404 Route GET:/api/admin/workspaces not found),只能通过命令行初始化:

node /srv/dittofeed/packages/admin-cli/dist/scripts/cli.js bootstrap \
  -n Tenant2 -t Parent -d tenant2.example.com

参数含义:-n 工作区名、-t 类型(Root 或 Parent,默认 Root)、-d 允许登录的邮箱域名。同名工作区再次执行是幂等更新,不会重复创建。

实测项 17:执行后 "Workspace" 表内容为

7bd6a519-18d7-4d26-8f4b-66b4788afa63 | Default | Root   |                     |
20dbc319-8f49-4147-989b-ddbdfb6ed290 | Tenant2 | Parent | tenant2.example.com |

初始化子工作区会同时写入一套默认密钥(default-write-key、各邮件/短信提供商占位、subscription-key),因此多工作区部署要先评估密钥表体积。

单租户模式下若请求未带 workspaceId,服务端回落到 SELECT ... FROM "Workspace" LIMIT 1 的第一条(见 packages/api/src/workspace.ts)。实测表中存在两个工作区时,回落结果取决于数据库返回顺序,因此单租户部署应只保留一个工作区,否则管理接口可能作用到错误的工作区。

十一、配置验证清单

#配置项验证命令/方法期望结果(本实例实测)
1DATABASE_NAMEpsql -c '\dt'dittofeed_src 下 31 张表,"Workspace" 有数据
2CLICKHOUSE_DATABASESHOW TABLES FROM <库>25 张表,user_events_v2 存在且事件可查
3TEMPORAL_ADDRESS启动 worker 看日志正常为 Worker created + RUNNING;错误地址抛 ConnectError
4API_PORT / API_HOSTcurl -i http://127.0.0.1:3101/api/200,日志 Server listening at http://0.0.0.0:3101
5APP_VERSIONcurl -s .../api/设置后 {"version":"0.24.0-alpha.21"},未设置为 {}
6API_BODY_LIMIT发 6 MB JSON413 FST_ERR_CTP_BODY_TOO_LARGE
7ALLOWED_ORIGINS带任意 Origin 请求恒为 access-control-allow-origin: *(该版本不生效)
8SECRET_KEY用旧 Cookie 访问新密钥实例401
9AUTH_MODE=anonymous访问 /api/public/single-tenant/login、POST /api/users404 与 200(无鉴权)
10AUTH_MODE=multi-tenant会话接口 /api/users500 Misconfigured auth provider, missing.
11PASSWORD空值启动后登录500 Application is misconfigured, contact support.
12SESSION_COOKIE_SECURE登录看 set-cookieHttpOnly; Secure
13LOG_LEVELgrep -c 'incoming request'error 级别为 0,info 级别成对出现
14BOOTSTRAPBOOTSTRAP=true 启动 api 后查询relation "workspace" does not exist(不迁移)
15WRITE_MODEtrack 后立即查事件ch-sync 79 ms、ch-async 141 ms 均可查到
16邮件提供商PUT /api/settings/email-providers201,"Secret".smtp 写入 configValue
17工作区bootstrap -n Tenant2 -t Parent"Workspace" 新增一行,类型 Parent

十二、实测发现的问题

问题现象(证据原文)建议
默认口令与默认签名密钥DEFAULT_BACKEND_CONFIG.password = "password"、secretKey 为固定常量部署前必须显式设置 PASSWORD、SECRET_KEY、两个数据库口令
配置日志泄漏口令LOG_CONFIG=true 时输出 "databasePassword":"Dit2feedPg2026"、"clickhousePassword":"Dit2feedCh2026"生产关闭 LOG_CONFIG,或限制日志文件权限
ALLOWED_ORIGINS 形同虚设任意 Origin 均返回 access-control-allow-origin: *白名单放到反向代理层
anonymous 模式裸奔POST /api/users 不带 Cookie 返回 200只允许单租户或多租户模式对外
多租户缺 AUTH_PROVIDER{"message":"Misconfigured auth provider, missing."}多租户部署必须配齐认证方式
BOOTSTRAP 不负责迁移relation "workspace" does not exist用 admin-cli bootstrap 初始化
仓库启动脚本陈旧scripts/bootstrap.sh 调用不存在的 packages/api/dist/scripts/bootstrap.js改用 admin-cli 入口
事件列名与 API 字段不一致Unknown expression identifier 'userId' ... Maybe you meant: ['user_id']自定义查询用 user_id
单租户回落依赖顺序GET ... FROM "Workspace" LIMIT 1单租户部署只保留一个工作区
健康检查不含版本未设 APP_VERSION 时 GET /api/ 返回 {}设置 APP_VERSION 供探针与排障使用

十三、参考