Open-source alternative
AppFlowy
The open-source alternative to Notion
AppFlowy is a free, self-hostable open-source alternative to Notion. Bring projects, wikis, and teams together with AI. AppFlowy is the AI collaborative workspace where you achieve more without losing control of your data. The leading open source Notion alternative. It is licensed under AGPL-3.0 and primarily written in Dart.
Feature comparison
Notion vs AppFlowy
| Feature | Notion | AppFlowy |
|---|---|---|
| Notes | ✓ | ✓ |
| Databases | ✓ | ✓ |
| Collaboration | ✓ | ✓ |
| Templates | ✓ | ✓ |
Docker Compose blueprint
# Essential services for AppFlowy Cloud
services:
nginx:
restart: on-failure
image: nginx
ports:
- ${NGINX_PORT:-80}:80 # Disable this if you are using TLS
- ${NGINX_TLS_PORT:-443}:443
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl/certificate.crt:/etc/nginx/ssl/certificate.crt
- ./nginx/ssl/private_key.key:/etc/nginx/ssl/private_key.key
# You do not need this if you have configured to use your own s3 file storage
minio:
restart: on-failure
image: minio/minio
environment:
- MINIO_BROWSER_REDIRECT_URL=${APPFLOWY_BASE_URL?:err}/minio
- MINIO_ROOT_USER=${APPFLOWY_S3_ACCESS_KEY:-minioadmin}
- MINIO_ROOT_PASSWORD=${APPFLOWY_S3_SECRET_KEY:-minioadmin}
command: server /data --console-address ":9001"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
interval: 30s
timeout: 20s
retries: 3
volumes:
- minio_data:/data
postgres:
restart: on-failure
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_DB=${POSTGRES_DB:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
- PGPORT=${POSTGRES_PORT:-5432}
command: [ "postgres", "-c", "port=${POSTGRES_PORT:-5432}" ]
healthcheck:
test: [ "CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}", "-p", "${POSTGRES_PORT:-5432}" ]
interval: 5s
timeout: 5s
retries: 12
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
restart: on-failure
image: redis
gotrue:
restart: on-failure
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: "curl --fail http://127.0.0.1:9999/health || exit 1"
interval: 5s
timeout: 5s
retries: 12
start_period: 40s
image: appflowyinc/gotrue:${GOTRUE_VERSION:-latest}
environment:
# There are a lot of options to configure GoTrue. You can reference the example config:
# https://github.com/supabase/auth/blob/master/example.env
# The initial GoTrue Admin user to create, if not already exists.
- GOTRUE_ADMIN_EMAIL=${GOTRUE_ADMIN_EMAIL}
# The initial GoTrue Admin user password to create, if not already exists.
# If the user already exists, the update will be skipped.
- GOTRUE_ADMIN_PASSWORD=${GOTRUE_ADMIN_PASSWORD}
- GOTRUE_DISABLE_SIGNUP=${GOTRUE_DISABLE_SIGNUP:-false}
- GOTRUE_SITE_URL=appflowy-flutter:// # redirected to AppFlowy application
- GOTRUE_URI_ALLOW_LIST=** # adjust restrict if necessary
- GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET} # authentication secret
- GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP}
# Without this environment variable, the createuser command will create an admin
# with the `admin` role as opposed to `supabase_admin`
- GOTRUE_JWT_ADMIN_GROUP_NAME=supabase_admin
- GOTRUE_DB_DRIVER=postgres
- API_EXTERNAL_URL=${API_EXTERNAL_URL}
- DATABASE_URL=${GOTRUE_DATABASE_URL}
- PORT=9999
- GOTRUE_SMTP_HOST=${GOTRUE_SMTP_HOST} # e.g. smtp.gmail.com
- GOTRUE_SMTP_PORT=${GOTRUE_SMTP_PORT} # e.g. 465
- GOTRUE_SMTP_USER=${GOTRUE_SMTP_USER} # email sender, e.g. noreply@appflowy.io
- GOTRUE_SMTP_PASS=${GOTRUE_SMTP_PASS} # email password
- GOTRUE_MAILER_URLPATHS_CONFIRMATION=/gotrue/verify
- GOTRUE_MAILER_URLPATHS_INVITE=/gotrue/verify
- GOTRUE_MAILER_URLPATHS_RECOVERY=/gotrue/verify
- GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/gotrue/verify
- GOTRUE_MAILER_TEMPLATES_MAGIC_LINK=${GOTRUE_MAILER_TEMPLATES_MAGIC_LINK}
- GOTRUE_SMTP_ADMIN_EMAIL=${GOTRUE_SMTP_ADMIN_EMAIL} # email with admin privileges e.g. internal@appflowy.io
- GOTRUE_SMTP_MAX_FREQUENCY=${GOTRUE_SMTP_MAX_FREQUENCY:-1ns} # set to 1ns for running tests
- GOTRUE_RATE_LIMIT_EMAIL_SENT=${GOTRUE_RATE_LIMIT_EMAIL_SENT:-100} # number of email sendable per minute
- GOTRUE_MAILER_AUTOCONFIRM=${GOTRUE_MAILER_AUTOCONFIRM:-false} # change this to true to skip email confirmation
# Google OAuth config
- GOTRUE_EXTERNAL_GOOGLE_ENABLED=${GOTRUE_EXTERNAL_GOOGLE_ENABLED}
- GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID}
- GOTRUE_EXTERNAL_GOOGLE_SECRET=${GOTRUE_EXTERNAL_GOOGLE_SECRET}
- GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=${GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI}
# GITHUB OAuth config
- GOTRUE_EXTERNAL_GITHUB_ENABLED=${GOTRUE_EXTERNAL_GITHUB_ENABLED}
- GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=${GOTRUE_EXTERNAL_GITHUB_CLIENT_ID}
- GOTRUE_EXTERNAL_GITHUB_SECRET=${GOTRUE_EXTERNAL_GITHUB_SECRET}
- GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI=${GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI}
# Discord OAuth config
- GOTRUE_EXTERNAL_DISCORD_ENABLED=${GOTRUE_EXTERNAL_DISCORD_ENABLED}
- GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=${GOTRUE_EXTERNAL_DISCORD_CLIENT_ID}
- GOTRUE_EXTERNAL_DISCORD_SECRET=${GOTRUE_EXTERNAL_DISCORD_SECRET}
- GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI=${GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI}
# SAML 2.0 OAuth config
- GOTRUE_SAML_ENABLED=${GOTRUE_SAML_ENABLED}
- GOTRUE_SAML_PRIVATE_KEY=${GOTRUE_SAML_PRIVATE_KEY}
appflowy_cloud:
restart: on-failure
environment:
- RUST_LOG=${RUST_LOG:-info}
- APPFLOWY_ENVIRONMENT=production
- APPFLOWY_DATABASE_URL=${APPFLOWY_DATABASE_URL}
- APPFLOWY_REDIS_URI=${APPFLOWY_REDIS_URI}
- APPFLOWY_GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET}
- APPFLOWY_GOTRUE_BASE_URL=${APPFLOWY_GOTRUE_BASE_URL}
- APPFLOWY_S3_CREATE_BUCKET=${APPFLOWY_S3_CREATE_BUCKET}
- APPFLOWY_S3_USE_MINIO=${APPFLOWY_S3_USE_MINIO}
- APPFLOWY_S3_MINIO_URL=${APPFLOWY_S3_MINIO_URL}
- APPFLOWY_S3_ACCESS_KEY=${APPFLOWY_S3_ACCESS_KEY}
- APPFLOWY_S3_SECRET_KEY=${APPFLOWY_S3_SECRET_KEY}
- APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET}
- APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION}
- APPFLOWY_S3_PRESIGNED_URL_ENDPOINT=${APPFLOWY_S3_PRESIGNED_URL_ENDPOINT}
- APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST}
- APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT}
- APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME}
- APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL}
- APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD}
- APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND}
- APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL}
- APPFLOWY_DATABASE_MAX_CONNECTIONS=${APPFLOWY_DATABASE_MAX_CONNECTIONS}
- AI_SERVER_HOST=${AI_SERVER_HOST}
- AI_SERVER_PORT=${AI_SERVER_PORT}
- AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
- APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL}
- APPFLOWY_BASE_URL=${APPFLOWY_BASE_URL}
- ASSEMBLYAI_API_KEY=${ASSEMBLYAI_API_KEY}
- ASSEMBLYAI_API_BASE=${ASSEMBLYAI_API_BASE}
- ASSEMBLYAI_STREAMING_API_BASE=${ASSEMBLYAI_STREAMING_API_BASE}
- APPFLOWY_SEARCH_SERVICE_URL=${APPFLOWY_SEARCH_SERVICE_URL:-http://appflowy_search:4002}
- APPFLOWY_SEARCH_REQUEST_TIMEOUT_SECS=${APPFLOWY_SEARCH_REQUEST_TIMEOUT_SECS:-10}
- AI_ENABLED=${AI_ENABLED:-true}
# SIGNUP_WHITELIST_ENABLED=true requires GOTRUE_DISABLE_SIGNUP=false on
# the gotrue service. The whitelist is enforced by GoTrue's
# BeforeUserCreated PG hook, which never runs when signup is globally
# disabled — leaving the whitelist a no-op. Set GOTRUE_DISABLE_SIGNUP=false
# and rely on the whitelist + invitations to gate who can sign up.
- SIGNUP_WHITELIST_ENABLED=${SIGNUP_WHITELIST_ENABLED:-false}
- GUEST_INVITES_REQUIRE_ADMIN_APPROVAL=${GUEST_INVITES_REQUIRE_ADMIN_APPROVAL:-false}
image: appflowyinc/appflowy_cloud:${APPFLOWY_CLOUD_VERSION:-latest}
healthcheck:
test: "curl --fail http://127.0.0.1:8000/api/health || exit 1"
interval: 5s
timeout: 5s
retries: 12
depends_on:
gotrue:
condition: service_healthy
admin_frontend:
restart: on-failure
image: appflowyinc/admin_frontend:${APPFLOWY_ADMIN_FRONTEND_VERSION:-latest}
environment:
- APPFLOWY_GOTRUE_BASE_URL=${APPFLOWY_GOTRUE_BASE_URL:-http://gotrue:9999}
- APPFLOWY_BASE_URL=${APPFLOWY_BASE_URL:-http://appflowy_cloud:8000}
depends_on:
gotrue:
condition: service_healthy
appflowy_cloud:
condition: service_healthy
ai:
restart: on-failure
image: appflowyinc/appflowy_ai:${APPFLOWY_AI_VERSION:-latest}
environment:
- AI_SERVER_PORT=${AI_SERVER_PORT}
- OPENAI_API_KEY=${AI_OPENAI_API_KEY}
- DEFAULT_AI_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account
- DEFAULT_AI_COMPLETION_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account
- AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}
- APPFLOWY_S3_ACCESS_KEY=${APPFLOWY_S3_ACCESS_KEY}
- APPFLOWY_S3_SECRET_KEY=${APPFLOWY_S3_SECRET_KEY}
- APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET}
- APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION}
- AI_DATABASE_URL=${APPFLOWY_DATABASE_URL}
- AI_REDIS_URL=${APPFLOWY_REDIS_URI}
- AI_USE_MINIO=${APPFLOWY_S3_USE_MINIO}
- AI_MINIO_URL=${APPFLOWY_S3_MINIO_URL}
- AI_APPFLOWY_HOST=${APPFLOWY_BASE_URL}
- APPFLOWY_GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET}
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:5001/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
postgres:
condition: service_healthy
appflowy_cloud:
condition: service_healthy
appflowy_worker:
restart: on-failure
image: appflowyinc/appflowy_worker:${APPFLOWY_WORKER_VERSION:-latest}
environment:
- RUST_LOG=${RUST_LOG:-info}
- APPFLOWY_ENVIRONMENT=production
- APPFLOWY_WORKER_REDIS_URL=${APPFLOWY_WORKER_REDIS_URL:-redis://redis:6379}
- APPFLOWY_WORKER_ENVIRONMENT=production
- APPFLOWY_WORKER_DATABASE_URL=${APPFLOWY_WORKER_DATABASE_URL}
- APPFLOWY_WORKER_DATABASE_NAME=${APPFLOWY_WORKER_DATABASE_NAME}
- APPFLOWY_WORKER_IMPORT_TICK_INTERVAL=30
- APPFLOWY_S3_USE_MINIO=${APPFLOWY_S3_USE_MINIO}
- APPFLOWY_S3_MINIO_URL=${APPFLOWY_S3_MINIO_URL}
- APPFLOWY_S3_ACCESS_KEY=${APPFLOWY_S3_ACCESS_KEY}
- APPFLOWY_S3_SECRET_KEY=${APPFLOWY_S3_SECRET_KEY}
- APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET}
- APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION}
- APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST}
- APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT}
- APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME}
- APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL}
- APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD}
- APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND}
depends_on:
postgres:
condition: service_healthy
appflowy_cloud:
condition: service_healthy
appflowy_search:
restart: on-failure
image: appflowyinc/appflowy_search:${APPFLOWY_SEARCH_VERSION:-latest}
environment:
- RUST_LOG=${RUST_LOG:-info}
- APPFLOWY_SEARCH_HOST=${APPFLOWY_SEARCH_HOST:-[::]}
- APPFLOWY_SEARCH_PORT=${APPFLOWY_SEARCH_PORT:-4002}
- APPFLOWY_SEARCH_DATABASE_URL=${APPFLOWY_DATABASE_URL}
- APPFLOWY_SEARCH_REDIS_URL=${APPFLOWY_REDIS_URI}
- AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
- AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}
# S3 / MinIO — required for /api/admin/search/.../rebuild to read collab
# blobs offloaded by appflowy_cloud. Without these the service falls back
# to MinIO defaults at localhost:9000 and the rebuild API returns
# "collab store is not configured; cannot rebuild documents".
- APPFLOWY_S3_USE_MINIO=${APPFLOWY_S3_USE_MINIO}
- APPFLOWY_S3_MINIO_URL=${APPFLOWY_S3_MINIO_URL}
- APPFLOWY_S3_ACCESS_KEY=${APPFLOWY_S3_ACCESS_KEY}
- APPFLOWY_S3_SECRET_KEY=${APPFLOWY_S3_SECRET_KEY}
- APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET}
- APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION}
- APPFLOWY_BACKGROUND_INDEXER_ENABLED=true
- APPFLOWY_INDEXER_DATABASE_ENABLED=${APPFLOWY_INDEXER_DATABASE_ENABLED:-false}
- APPFLOWY_KEYWORD_SEARCH_ENABLED=${APPFLOWY_KEYWORD_SEARCH_ENABLED:-true}
- APPFLOWY_KEYWORD_WORKER_ENABLED=true
- APPFLOWY_KEYWORD_INDEX_MAP_SIZE_BYTES=${APPFLOWY_KEYWORD_INDEX_MAP_SIZE_BYTES:-2147483648}
- APPFLOWY_KEYWORD_INDEX_DIR=${APPFLOWY_KEYWORD_INDEX_DIR:-/var/lib/appflowy/keyword_index}
- APPFLOWY_GOTRUE_JWT_SECRET=${GOTRUE_JWT_SECRET}
volumes:
- keyword_index_data:/var/lib/appflowy/keyword_index
healthcheck:
# The runtime image does not ship curl or wget. Bash's /dev/tcp support
# keeps this an HTTP-level check without adding an image dependency.
test:
- CMD
- bash
- -ec
- |
exec 3<>/dev/tcp/127.0.0.1/$${APPFLOWY_SEARCH_PORT:-4002}
printf 'GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3
read -r _ status _ <&3
[ "$$status" = 200 ]
interval: 5s
timeout: 5s
retries: 12
start_period: 40s
depends_on:
postgres:
condition: service_healthy
appflowy_web:
restart: on-failure
image: appflowyinc/appflowy_web:${APPFLOWY_WEB_VERSION:-latest}
depends_on:
appflowy_cloud:
condition: service_healthy
environment:
- APPFLOWY_BASE_URL=${APPFLOWY_BASE_URL}
- APPFLOWY_GOTRUE_BASE_URL=${APPFLOWY_BASE_URL}/gotrue
- APPFLOWY_WS_BASE_URL=${APPFLOWY_WEBSOCKET_BASE_URL}
volumes:
postgres_data:
minio_data:
keyword_index_data:
One-click deploy
Spin up AppFlowy on a managed platform.
Why choose AppFlowy over Notion?
AppFlowy is a community-driven open-source project that offers a self-hostable alternative to Notion. With a health score of 85.0/100, an AGPL-3.0 license, and full control over your data, it is built for teams that value privacy and ownership.
- ✓ Self-hostable — run it on your own servers or cloud.
- ✓ Open license — audit, fork, and customize freely.
- ✓ No vendor lock-in — export and migrate your data.
- ✓ Active community — issues and PRs on GitHub.
Self-hosting guide
-
1
Clone or use the Docker blueprint
Use the Compose file above, or clone the repo from GitHub and follow upstream docs.
-
2
Configure environment variables
Set database, secrets, and app URL. Never commit production secrets to git.
-
3
Add reverse proxy & SSL
Put Nginx or Caddy in front for HTTPS, rate limiting, and static caching.
-
4
Backups & updates
Schedule volume/database backups and watch GitHub releases for security patches.
Self-host difficulty for AppFlowy is rated 3/5.