AFFiNE 0.27.4 配置参数详解
AFFiNE 0.27.4 配置体系全解:环境变量与 config.json 的优先级、server/auth/mailer/storages/copilot/indexer/job/throttle 各模块字段与默认值、生效配置 appConfig 查询方法、生产模板与六类排障实录,字段清单以官方 config.schema.json 为准。
AFFiNE 0.27.4 配置参数详解
目标:把 AFFiNE 0.27.4 的全部配置入口讲透——
config.json有哪些模块、每个字段的默认值是什么、哪些能用环境变量覆盖、改完之后怎么验证真正生效。字段清单以官方config.schema.json(2026-10-25 取回)为准,默认值与生效值以本文实例的appConfig实测为证。
一、环境信息
- 操作系统:Ubuntu 22.04(x86_64)
- Docker Engine:29.9.2,Docker Compose 插件 v5.6.1
- AFFiNE 版本:0.27.4(
ghcr.io/toeverything/affine:0.27.4,allinone 形态) - 依赖版本:PostgreSQL 16(
pgvector/pgvector:pg16)、Redis 7(实际运行 7.4.11) - 部署目录:
/opt/data/quzuo/tmp/affine/deploy/ - 配置目录:宿主机
./config→ 容器内/root/.affine/config - 数据目录:宿主机
./data/storage→ 容器内/root/.affine/storage - 对外地址:
http://192.168.146.128:8137 - 管理员:
admin@example.com/q1w2e3r4 - 配置 schema:
config.schema.json(62,656 字节,draft-07) - 部署过程见 《AFFiNE 0.27.4 Docker Compose 部署指南》
二、配置体系总览
AFFiNE 的配置分三层,优先级从高到低:
环境变量 > config.json > 内置默认值
三层各自的定位:
| 层 | 位置 | 适合放什么 |
|---|---|---|
| 环境变量 | 编排文件的 environment | 连接串、密码、单值开关(DATABASE_URL、REDIS_SERVER_HOST、AFFINE_INDEXER_ENABLED) |
config.json | 容器内 /root/.affine/config/config.json | 结构化配置(server、auth、mailer、oauth、copilot 等) |
| 内置默认值 | 应用代码内 | 未显式配置时兜底,完整清单见官方 schema |
三条硬规则:
config.json在进程启动时读取,修改后必须重启应用进程才生效,热加载不存在。- 文件必须是合法 JSON,不能写注释、不能有尾逗号;语法错误会导致启动失败,排障时先看启动日志的解析报错。
config.json不是必填项——没有该文件时应用按默认值启动,但生产环境几乎一定要写,至少要把server.externalUrl配对。
同目录下还有两个伴随文件:
private.key:首次启动自动生成的 EC 私钥,对应crypto.privateKey(schema 标注可用AFFINE_PRIVATE_KEY覆盖)。不要删,删掉会导致已签发的令牌全部失效,且多副本部署时各副本私钥不一致会互相拒绝令牌。config.schema.json不随部署生成,需要从官方 Release 下载后放在本地做编辑器校验(VSCode 的 JSON Schema 联想)。
💡 技术拓展:AFFiNE 用 JSON Schema(draft-07)描述配置,而不是自定义解析器。好处是编辑器能实时提示字段、类型与默认值;坏处是「schema 里有」不等于「当前版本已实现」——例如
auth.requireEmailVerification默认true,但 schema 描述明确写着(not implemented),配了也不会真的拦截未验证邮箱。读配置时要连描述一起读,不能只看字段名。
三、配置生效值怎么查
配置是否真正生效,不用猜,登录后查 GraphQL 的 appConfig 即可拿到合并默认值后的完整生效配置。先登录拿 Cookie:
curl -s -c ai_cookies.txt -X POST http://127.0.0.1:8137/api/auth/sign-in \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"q1w2e3r4"}' \
-o /dev/null -w "login HTTP %{http_code}\n"
实测输出:
login HTTP 200
再查生效配置:
curl -s -b ai_cookies.txt -X POST http://127.0.0.1:8137/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ appConfig }"}'
返回一个 JSON 对象,顶层键为:
redis, metrics, graphql, crypto, job, throttle, auth, mailer, doc, storages,
websocket, db, server, flags, docService, telemetry, client, calendar,
captcha, copilot, indexer, oauth, payment, worker
只看关键几项:
curl -s -b ai_cookies.txt -X POST http://127.0.0.1:8137/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ serverConfig { name features } }"}'
实测输出:
{
"data": {
"serverConfig": {
"name": "AFFiNE 内网知识库",
"features": ["Comment", "Copilot", "LocalWorkspace", "CopilotEmbedding"]
}
}
}
serverConfig.name 对应 config.json 的 server.name;features 是服务端实际开启的能力集合,Copilot 出现即证明 copilot.enabled 已被读到。这个方法比翻日志快得多,也是本文所有「生效值」的来源。
⚠️ 注意:
appConfig会返回crypto.privateKey明文。不要在共享终端、录屏或截图里直接展示完整响应;需要贴图时先脱敏。
四、server 模块
| 字段 | 类型 | 默认值 | 环境变量 | 说明 |
|---|---|---|---|---|
server.name | string | 无 | — | 服务名,桌面端连接时展示 |
server.externalUrl | string | '' | — | 对外基础 URL,用于生成分享链接、邀请链接、头像地址 |
server.https | boolean | false | AFFINE_SERVER_HTTPS | 是否以 HTTPS 对外提供服务 |
server.host | string | 'localhost' | AFFINE_SERVER_HOST | 部署主机名(FQDN) |
server.hosts | array | [] | — | 额外接受的主机名列表 |
server.listenAddr | string | '0.0.0.0' | LISTEN_ADDR | 监听地址 |
server.port | number | 3010 | AFFINE_SERVER_PORT | 监听端口 |
server.path | string | '' | AFFINE_SERVER_SUB_PATH | 子路径部署前缀(如 /affine) |
本文实例的生效值(appConfig.server 实测):
{"name":"AFFiNE 内网知识库","externalUrl":"http://192.168.146.128:8137","https":false,
"host":"localhost","hosts":[],"listenAddr":"0.0.0.0","port":3010,"path":""}
三个容易踩的点:
externalUrl必须写对。头像 URL、邀请链接、OAuth 回调都基于它拼接。本文实例登录后返回的头像地址是http://192.168.146.128:8137/api/avatars/...,正是externalUrl的值——写成localhost会导致其他机器打开链接失败。host没配也显示localhost。这是默认值,不代表实际访问地址;决定外部访问的是externalUrl和端口映射。不要在两者之间二选一,正确做法是externalUrl写实际访问地址,host保持默认或写 FQDN。- 反代场景改
https与path。放在 Nginx 后面且启用 TLS 时,externalUrl用https://域名,server.https置true;挂在子路径时同时设path,并确保反代正确剥离前缀。
五、auth 模块
| 字段 | 默认值 | 说明 |
|---|---|---|
auth.allowSignup | true | 是否允许自助注册 |
auth.allowSignupForOauth | true | 是否允许通过 OAuth 首次登录时自动建号 |
auth.requireEmailDomainVerification | false | 访问受限资源前是否校验邮箱域名记录 |
auth.requireEmailVerification | true | 是否要求邮箱验证(schema 注明未实现) |
auth.newAccountShareActionDelay | 86400 | 新账号多少秒后才能邀请成员/建分享链接(24 小时) |
auth.passwordRequirements.min / .max | 8 / 32 | 密码长度上下限 |
auth.session.ttl | 1296000 | 会话有效期(15 天) |
auth.session.ttr | 604800 | 会话刷新窗口(7 天) |
auth.token.accessTokenTtl | 900 | 访问令牌有效期(15 分钟) |
auth.token.refreshIdleTtl | 2592000 | 刷新会话闲置过期(30 天) |
auth.token.refreshAbsoluteTtl | 15552000 | 刷新会话绝对过期(180 天) |
auth.token.refreshGracePeriod | 30 | 一次性刷新轮换的并发宽限(30 秒) |
auth.token.refreshRetention | 2592000 | 过期刷新代的保留时长(30 天) |
本文实例生效值(节选):
{"allowSignup":true,"allowSignupForOauth":true,"requireEmailDomainVerification":false,
"requireEmailVerification":true,"newAccountShareActionDelay":86400,
"passwordRequirements":{"min":8,"max":32},
"session":{"ttl":1296000,"ttr":604800},
"token":{"accessTokenTtl":900,"refreshIdleTtl":2592000,"refreshAbsoluteTtl":15552000,
"refreshGracePeriod":30,"refreshRetention":2592000}}
生产建议:
- 内网/团队实例把
allowSignup关掉,成员由管理员邀请,避免陌生人注册后占用工作区配额。 - 密码策略只改长度,不涉及复杂度校验;需要更强约束时应在外层 SSO 或反向代理做。
newAccountShareActionDelay是反滥用参数,默认 24 小时;开放注册的实例不要调成 0。
登录接口本身不是 GraphQL,而是 REST:
curl -s -X POST http://127.0.0.1:8137/api/auth/sign-in \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"q1w2e3r4"}' \
-w "\nHTTP %{http_code}\n"
实测输出:
{"id":"38ccef6f-61d0-4a00-ae0b-da659babdf06","email":"admin@example.com","avatarUrl":"http://192.168.146.128:8137/api/avatars/38ccef6f-61d0-4a00-ae0b-da659babdf06-avatar-1792902086797","name":"admin","disabled":false,"hasPassword":true,"emailVerified":false}
HTTP 200
返回体里 avatarUrl 直接暴露了 externalUrl 的拼接结果,是检查该项配置最简单的窗口。
六、mailer 模块
邮件用于邀请成员、找回密码、修改邮箱。未配置 SMTP 时这些功能会静默失败(接口成功、邮件不发出),这是自托管最常见的“功能好像坏了”。
| 字段 | 默认值 | 环境变量 |
|---|---|---|
mailer.SMTP.name | '' | MAILER_SERVERNAME |
mailer.SMTP.host | '' | MAILER_HOST |
mailer.SMTP.port | 465 | MAILER_PORT |
mailer.SMTP.username | '' | MAILER_USER |
mailer.SMTP.password | '' | MAILER_PASSWORD |
mailer.SMTP.sender | 'AFFiNE Self Hosted <noreply@example.com>' | MAILER_SENDER |
mailer.SMTP.ignoreTLS | false | MAILER_IGNORE_TLS |
mailer.fallbackDomains | [] | — |
投递工作器参数(默认值):batchSize 50、leaseMs 120000、retentionDays 30。
fallbackSMTP 结构与 SMTP 相同,用于「按收件域名分流」:把域名加进 fallbackDomains,这些域名的邮件改走备用服务器。典型场景是主 SMTP 只允许发内部域,外部收件人走另一条通道。
配置示例:
{
"mailer": {
"SMTP": {
"host": "smtp.example.com",
"port": 465,
"username": "noreply@example.com",
"password": "${SMTP_PASSWORD}",
"sender": "AFFiNE <noreply@example.com>",
"ignoreTLS": false
}
}
}
注意 config.json 不支持变量插值,上面 ${SMTP_PASSWORD} 只是示意,实际应直接写值,或用环境变量 MAILER_PASSWORD 覆盖(环境变量层优先级更高,也不落盘,推荐后者)。
七、storages 模块
| 字段 | 默认值 | 说明 |
|---|---|---|
storages.avatar.publicPath | '/api/avatars/' | 头像公开访问前缀 |
storages.avatar.storage | {provider:'fs',bucket:'avatars',config:{path:'~/.affine/storage'}} | 头像存储 |
storages.blob.storage | {provider:'fs',bucket:'blobs',config:{path:'~/.affine/storage'}} | 附件/图片/视频存储 |
本文实例生效值:
avatar: {publicPath:/api/avatars/, storage:{provider:fs, bucket:avatars, config:{path:~/.affine/storage}}}
blob: {storage:{provider:fs, bucket:blobs, config:{path:~/.affine/storage}}}
~/.affine/storage 在容器内即 /root/.affine/storage,由编排文件挂到宿主机 ./data/storage。avatar 与 blob 用不同 bucket,落盘时是同级子目录,互不干扰,备份时整目录打包即可。
要换成 S3/MinIO 等对象存储,把 provider 改为对应驱动并补 config(endpoint、bucket、密钥),bucket 字段仍要保留用于逻辑隔离。切换前先迁移已有对象,否则历史头像与附件 404。
八、copilot 模块
| 字段 | 默认值 | 说明 |
|---|---|---|
copilot.enabled | false | AI 功能总开关 |
copilot.byok.enabled | true | 允许工作区管理员自带模型密钥 |
copilot.byok.allowedProviders | ["openai","anthropic","gemini","fal"] | 允许接入的提供商 |
copilot.byok.allowCustomEndpoint | false | 是否允许自定义 API 端点 |
copilot.byok.allowPrivateEndpoint | false | 是否允许端点解析到内网地址 |
copilot.unsplash.key | '' | Unsplash 图库密钥 |
copilot.exa.key | '' | Exa 联网搜索密钥 |
copilot.storage | {provider:'fs',bucket:'copilot',config:{path:'~/.affine/storage'}} | AI 附件存储 |
本文实例把 copilot.enabled 设为 true,并开启 BYOK 与自定义端点:
{
"copilot": {
"enabled": true,
"byok": { "enabled": true, "allowCustomEndpoint": true }
}
}
对应 serverConfig.features 里出现 Copilot,说明开关已生效。
两个安全边界要留意:
allowCustomEndpoint放开后,管理员可以填任意地址(包括http://的内网服务);allowPrivateEndpoint默认false就是拦这个。两者一起开等于允许工作区管理员做内网探测,只应在完全可信的单租户环境开启。enabled: true不等于「AI 可用」。还需要在「工作区设置 → 集成 → AI BYOK」里填至少一个提供商的密钥;没填时前端会提示未配置,而不是报服务端错误。
九、indexer 模块
全文检索是可选的,默认关闭;打开后需要一个外部搜索引擎。
| 字段 | 默认值 | 环境变量 |
|---|---|---|
indexer.enabled | false | AFFINE_INDEXER_ENABLED |
indexer.provider.type | 'manticoresearch' | AFFINE_INDEXER_SEARCH_PROVIDER |
indexer.provider.endpoint | 'http://localhost:9308' | AFFINE_INDEXER_SEARCH_ENDPOINT |
indexer.provider.apiKey | '' | AFFINE_INDEXER_SEARCH_API_KEY |
indexer.provider.username / .password | '' | AFFINE_INDEXER_SEA...(超长截断,见 schema) |
indexer.autoIndex.batchSize | 10 | — |
本文实例在编排文件里显式关闭:
environment:
- AFFINE_INDEXER_ENABLED=false
生效值:
{"enabled":false,"provider":{"type":"manticoresearch","endpoint":"http://localhost:9308","apiKey":"","username":"","password":""},"autoIndex":{"batchSize":10}}
注意 provider 的默认值仍然存在(manticoresearch + localhost:9308),只是 enabled:false 让其不参与工作。启用前必须先部署 Manticore Search 或 Elasticsearch,否则文档索引任务会持续失败并刷日志。
十、job 与 throttle 模块
10.1 job(队列)
默认队列级参数:
| 字段 | 默认值 |
|---|---|
job.queue.attempts | 5 |
job.queue.backoff.type / .delay | exponential / 1000 ms |
job.queue.removeOnComplete | true |
job.queue.removeOnFail.age / .count | 86400 / 500 |
job.worker | {}(BullMQ WorkerOptions 透传) |
各队列并发默认值:
| 队列 | 并发 |
|---|---|
copilot | 10 |
calendar | 4 |
notification | 10 |
doc | 1 |
indexer | 1 |
nightly | 1 |
backendRuntime | 1 |
inviteAbuse | 1 |
队列依赖 Redis。本文实例通过 REDIS_SERVER_HOST=redis 指向同项目的 Redis 服务,appConfig.redis 实测为:
{"db":0,"host":"redis","port":6379,"username":"","password":"","ioredis":{}}
小规模部署(几十人)无需调并发;文档导入或批处理多的时候可把 doc 队列调到 2–4,但要相应增加内存——文档合并峰值约 1 GB/任务。
10.2 throttle(限流)
| 字段 | 默认值 | 说明 |
|---|---|---|
throttle.enabled | true | 是否启用限流 |
throttle.throttlers.default.ttl / .limit | 60000 / 120 | 普通接口:60 秒 120 次 |
throttle.throttlers.strict.ttl / .limit | 60000 / 20 | 严格接口:60 秒 20 次 |
本文实例生效值:
{"enabled":true,"throttlers":{"default":{"ttl":60000,"limit":120},"strict":{"ttl":60000,"limit":20}}}
限流计数落在 Redis,多副本共享同一计数。反向代理后面部署时,要确保真实客户端 IP 能透传,否则所有请求会被算作同一个来源,更容易触发 429。
十一、websocket、doc、client、flags、telemetry
| 模块 | 字段 | 默认值 | 说明 |
|---|---|---|---|
websocket | transports | ["websocket","polling"] | 允许的传输方式 |
websocket | maxHttpBufferSize | 100000000 | 单消息上限(约 95 MB) |
doc | experimental.yocto | false | 用 y-octo 合并 Yjs 更新 |
doc | history.interval | 600000 | 历史快照最小间隔(10 分钟) |
client | versionControl.enabled | false | 是否校验客户端版本 |
client | versionControl.requiredVersion | '>=0.25.0' | 允许的版本范围 |
flags | allowGuestDemoWorkspace | true | 游客可建演示工作区 |
telemetry | allowedOrigin | ["localhost","127.0.0.1"] | 允许上报的来源 |
telemetry | ga4.measurementId / .apiSecret | '' | GA4 Measurement Protocol |
maxHttpBufferSize 默认值很大(100 MB),意味着单个超大附件可能占满内存。附件走的是分片上传(blob 接口),正常文档协作不会接近这个量级;如果做压力测试,建议临时调低到 10 MB 观察表现。
client.versionControl.enabled 打开后,低于 requiredVersion 的桌面/网页客户端会被拒绝,适合强制全员升级;升级期间先把范围放宽,避免把用户挡在门外。
十二、oauth、calendar、captcha、payment
这四个模块默认全关,只有填了第三方凭据才需要关注。
| 模块 | 关键字段 | 默认 | 启用条件 |
|---|---|---|---|
oauth.providers.google | clientId / clientSecret | 空 | Google OAuth 应用 |
oauth.providers.github | clientId / clientSecret | 空 | GitHub OAuth App |
oauth.providers.oidc | clientId / clientSecret / issuer | 空 | 标准 OIDC 提供商;内网 issuer 需 allowPrivateNetwork:true |
oauth.providers.apple | clientId / clientSecret | 空 | Apple 登录 |
calendar.google | enabled / clientId / clientSecret 等 | enabled:false | Google Calendar 集成 |
calendar.caldav | enabled / allowedHosts / blockPrivateNetwork | enabled:false,blockPrivateNetwork:true | CalDAV 集成 |
captcha.enabled | 布尔 | false | 防机器人;支持 Turnstile 或本地挑战(challenge.bits:20) |
payment.enabled | 布尔 | false | 自托管不需要,保持关闭 |
calendar.caldav.blockPrivateNetwork 默认 true,即默认禁止连接内网 CalDAV 服务;若你的日历服务就在内网,需要显式关闭该保护并把主机加入 allowedHosts——这是一处典型的安全默认值与内网需求冲突,改动时要想清楚暴露面。
十三、配置生效验证清单
改完配置后按顺序验证,三步即可定位问题:
- 进程起来了没:
GET /info返回版本与形态。
curl -s -D - http://127.0.0.1:8137/info -o /tmp/ainfo.json -w "\nHTTP %{http_code}\n" | head -5
cat /tmp/ainfo.json
实测输出:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
{"compatibility":"0.27.4","message":"AFFiNE 0.27.4 Server","type":"selfhosted","flavor":"allinone"}
-
配置读到了没:登录后查
appConfig对应模块,确认值不是默认值。本文实例server.externalUrl为自定义地址、copilot.enabled为true、indexer.enabled为false,三者都能在appConfig中一一对上。 -
功能特性开了没:查
serverConfig.features,Copilot在列说明 AI 开关生效;再点进具体功能页做一次真实操作(例如邀请一位成员看邮件是否到达)。
如果第 1 步失败,看启动日志的 JSON 解析错误;如果第 2 步值没变,九成是改了宿主机的 config.json 但没重启应用进程,或者改的是另一个目录的同名文件。
十四、生产配置模板
结合本文实例与默认值,一份内网生产配置如下(SMTP 与 OAuth 按需替换,未用的模块直接省略):
{
"$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
"server": {
"name": "团队知识库",
"externalUrl": "https://affine.example.com",
"https": true
},
"auth": {
"allowSignup": false,
"passwordRequirements": { "min": 10, "max": 32 }
},
"mailer": {
"SMTP": {
"host": "smtp.example.com",
"port": 465,
"username": "noreply@example.com",
"password": "从环境变量 MAILER_PASSWORD 注入",
"sender": "团队知识库 <noreply@example.com>"
}
},
"copilot": {
"enabled": true,
"byok": { "enabled": true, "allowCustomEndpoint": false }
},
"doc": {
"history": { "interval": 600000 }
},
"throttle": {
"enabled": true
}
}
对应编排文件里补环境变量(不落盘敏感值):
environment:
- DATABASE_URL=postgresql://affine:<密码>@postgres:5432/affine
- REDIS_SERVER_HOST=redis
- MAILER_PASSWORD=<SMTP 密码>
- AFFINE_INDEXER_ENABLED=false
十五、常见问题
15.1 改了 config.json 没反应
config.json 只在启动时读一次。确认三件事:改的是挂载进容器的那个文件(宿主机 ./config/config.json,不是镜像内副本)、JSON 语法合法、应用进程已重启。用 appConfig 对比改前改后,值不变就是没重启。
15.2 邀请链接/头像地址是 localhost
server.externalUrl 没配或配错。登录接口返回的 avatarUrl 会直接暴露这个问题:把 externalUrl 改成实际访问地址后重启,再看返回值。
15.3 邀请成员提示成功但对方没收到邮件
mailer.SMTP 为空,投递被跳过。至少配 host、port、username、password、sender 五项;自建邮件服务器用自签证书时设 ignoreTLS: true,否则 TLS 校验失败。
15.4 开启 Copilot 后 AI 仍不可用
copilot.enabled 只是总开关,具体密钥在工作区设置里配置。确认 serverConfig.features 含 Copilot,再到「工作区设置 → 集成 → AI BYOK」填提供商密钥;若使用自定义端点,还需服务端 allowCustomEndpoint: true。
15.5 打开全文搜索没有结果
indexer.enabled 仍为 false(本文实例即如此),或启用了但 Manticore/Elasticsearch 未就绪。先确认索引服务可访问 provider.endpoint,再打开开关,否则任务会持续失败。
15.6 删了 private.key 后所有人都要重新登录
private.key 是令牌签名私钥,删除后应用会重新生成,旧令牌全部失效。恢复方式是从备份还原该文件;没有备份就只能让用户重新登录。多副本部署时各副本必须共用同一把私钥,建议把整个 config 目录纳入备份。
十六、参考链接
- 站内:《AFFiNE 0.27.4 介绍与选型指南》、《AFFiNE 0.27.4 Docker Compose 部署指南》
- 官方配置 schema:
https://github.com/toeverything/AFFiNE/releases/latest/download/config.schema.json - 官方自托管文档:
https://docs.affine.pro/docs/self-host-affine - 字段与默认值取回日期:2026-10-25;生效值来自本文实例 0.27.4 的
appConfig查询