diff --git a/README.md b/README.md index 301a976d39..fdb6e976b2 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ If you have a spare domain name you can configure applications to be accessible * [Sonarr](https://sonarr.tv/) - for downloading and managing TV episodes * [Speedtest-Tracker](https://github.com/henrywhitaker3/Speedtest-Tracker) - Continuously track your internet speed * Stats - Monitor and visualise metrics about your NAS and internet connection using Grafana, Prometheus, Telegraf and more. +* [Standard Notes](https://standardnotes.com/) - An end-to-end encrypted notes app * [Stirling-PDF](https://github.com/Frooodle/Stirling-PDF) - locally hosted web application that allows you to perform various operations on PDF files * [Syncthing](https://syncthing.net/) - sync directories with another device * [Tautulli](http://tautulli.com/) - Monitor Your Plex Media Server diff --git a/nas.yml b/nas.yml index 7cc39a3da4..236e06fa9a 100644 --- a/nas.yml +++ b/nas.yml @@ -606,6 +606,10 @@ tags: - speedtest-tracker + - role: standardnotes + tags: + - standardnotes + - role: stats tags: - stats diff --git a/roles/standardnotes/defaults/main.yml b/roles/standardnotes/defaults/main.yml new file mode 100644 index 0000000000..d14c11aabd --- /dev/null +++ b/roles/standardnotes/defaults/main.yml @@ -0,0 +1,88 @@ +--- +standardnotes_enabled: false +standardnotes_available_externally: false +standardnotes_app_client_enabled: false +standardnotes_enable_subscription: false + +# directories +standardnotes_data_directory: "{{ docker_home }}/standardnotes" + +# network +standardnotes_port: "3011" +standardnotes_files_port: "3013" +standardnotes_app_port: "8128" +standardnotes_app_hostname: "standardnotes" +standardnotes_server_hostname: "standardnotes-server" +standardnotes_files_hostname: "standardnotes-files" +standardnotes_network_name: "standardnotes" + +# specs +standardnotes_memory: 1g +standardnotes_localstack_memory: 1g +standardnotes_db_memory: 1g +standardnotes_redis_memory: 1g +standardnotes_app_memory: 4g + +# docker +standardnotes_container_name: standardnotes +standardnotes_image_name: "standardnotes/server" +standardnotes_image_version: latest + +standardnotes_app_container_name: standardnotes-app +standardnotes_app_image_name: "ghcr.io/jackyzy823/standardnotes-web" +standardnotes_app_image_version: latest + +standardnotes_localstack_container_name: standardnotes-localstack +standardnotes_localstack_image_name: "localstack/localstack" +standardnotes_localstack_image_version: "1.4" + +standardnotes_db_container_name: standardnotes-db +standardnotes_db_image_name: "mysql" +standardnotes_db_image_version: "8" + +standardnotes_redis_container_name: standardnotes-redis +standardnotes_redis_image_name: "redis" +standardnotes_redis_image_version: "6.0-alpine" +standardnotes_user_id: "1000" +standardnotes_group_id: "1000" + +# standardnotes +standardnotes_db_database: "standardnotes" +standardnotes_db_user: "standardnotes" +standardnotes_db_root_password: "supersecure" +standardnotes_db_password: "changeme" +standardnotes_db_host: "{{ standardnotes_db_container_name }}" +standardnotes_db_port: "3306" +standardnotes_db_type: "mysql" +standardnotes_db_charset: "utf8mb4" +standardnotes_db_lang: "C.UTF-8" + +######### +# CACHE # +######### + +standardnotes_redis_port: "6379" +standardnotes_redis_host: "{{ standardnotes_redis_container_name }}" +standardnotes_cache_type: "redis" + +######## +# KEYS # +######## + +standardnotes_jwt_secret: "change_me1" +standardnotes_encryption_server_key: "change_me2" +standardnotes_valet_token_secret: "change_me3" + +####### +# APP # +####### +standardnotes_sf_default_server: "https://{{ standardnotes_server_hostname }}.{{ ansible_nas_domain }}" +standardnotes_app_env_port: "3001" +standardnotes_app_host: "https://{{ standardnotes_app_hostname }}.{{ ansible_nas_domain }}" +# Subscription related endpoints +standardnotes_app_dashboard_url: "http://standardnotes.com/dashboard" +standardnotes_app_plans_url: "https://standardnotes.com/plans" +standardnotes_app_purchase_url: "https://standardnotes.com/purchase" + +standardnotes_subscription_email: "" +standardnotes_public_files_server_url: "https://{{ standardnotes_files_hostname }}.{{ ansible_nas_domain }}" diff --git a/roles/standardnotes/docs/standardnotes.md b/roles/standardnotes/docs/standardnotes.md new file mode 100644 index 0000000000..6bf2be0cc6 --- /dev/null +++ b/roles/standardnotes/docs/standardnotes.md @@ -0,0 +1,15 @@ +# Standard Notes + +Homepage: + +Standard Notes is a free, secure note-taking app with powerful end-to-end encryption, unparalleled privacy features, and seamless cross-platform syncing on unlimited devices. + +## Usage + +Set `standardnotes_enabled: true` in your `inventories//nas.yml` file to install Standard Notes sync server. + +Standard Notes sync server interface can be then found at . + +Optionally, set `standardnotes_app_client_enabled: true` to install Standard Notes Client Web App. + +After server installation and creating of admin user, set `standardnotes_subscription_email` to the admin user email, set `standardnotes_enable_subscription: true` and re-run the playbook to enable subscription features. diff --git a/roles/standardnotes/molecule/default/molecule.yml b/roles/standardnotes/molecule/default/molecule.yml new file mode 100644 index 0000000000..729c7ce8e2 --- /dev/null +++ b/roles/standardnotes/molecule/default/molecule.yml @@ -0,0 +1,7 @@ +--- +provisioner: + inventory: + group_vars: + all: + standardnotes_enabled: true + standardnotes_app_client_enabled: true diff --git a/roles/standardnotes/molecule/default/side_effect.yml b/roles/standardnotes/molecule/default/side_effect.yml new file mode 100644 index 0000000000..5e0913d21b --- /dev/null +++ b/roles/standardnotes/molecule/default/side_effect.yml @@ -0,0 +1,10 @@ +--- +- name: Stop + hosts: all + become: true + tasks: + - name: "Include {{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }} role" + ansible.builtin.include_role: + name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + standardnotes_enabled: false diff --git a/roles/standardnotes/molecule/default/verify.yml b/roles/standardnotes/molecule/default/verify.yml new file mode 100644 index 0000000000..230a114d35 --- /dev/null +++ b/roles/standardnotes/molecule/default/verify.yml @@ -0,0 +1,47 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Include vars + ansible.builtin.include_vars: + file: ../../defaults/main.yml + + - name: Get standardnotes localstack container state + community.docker.docker_container: + name: "{{ standardnotes_localstack_container_name }}" + register: result_localstack + + - name: Get standardnotes db container state + community.docker.docker_container: + name: "{{ standardnotes_db_container_name }}" + register: result_db + + - name: Get standardnotes redis container state + community.docker.docker_container: + name: "{{ standardnotes_redis_container_name }}" + register: result_redis + + - name: Get standardnotes container state + community.docker.docker_container: + name: "{{ standardnotes_container_name }}" + register: result + + - name: Get standardnotes web app container state + community.docker.docker_container: + name: "{{ standardnotes_app_container_name }}" + register: result_app + + - name: Check if standardnotes containers are running + ansible.builtin.assert: + that: + - result_db.container['State']['Status'] == "running" + - result_db.container['State']['Restarting'] == false + - result_localstack.container['State']['Status'] == "running" + - result_localstack.container['State']['Restarting'] == false + - result_redis.container['State']['Status'] == "running" + - result_redis.container['State']['Restarting'] == false + - result.container['State']['Status'] == "running" + - result.container['State']['Restarting'] == false + - result_app.container['State']['Status'] == "running" + - result_app.container['State']['Restarting'] == false diff --git a/roles/standardnotes/molecule/default/verify_stopped.yml b/roles/standardnotes/molecule/default/verify_stopped.yml new file mode 100644 index 0000000000..5138cc04bf --- /dev/null +++ b/roles/standardnotes/molecule/default/verify_stopped.yml @@ -0,0 +1,47 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Include vars + ansible.builtin.include_vars: + file: ../../defaults/main.yml + + - name: Try and stop and remove standardnotes + community.docker.docker_container: + name: "{{ standardnotes_container_name }}" + state: absent + register: result + + - name: Try and stop and remove standardnotes redis + community.docker.docker_container: + name: "{{ standardnotes_redis_container_name }}" + state: absent + register: result_redis + + - name: Try and stop and remove standardnotes db + community.docker.docker_container: + name: "{{ standardnotes_db_container_name }}" + state: absent + register: result_db + + - name: Try and stop and remove standardnotes localstack + community.docker.docker_container: + name: "{{ standardnotes_localstack_container_name }}" + state: absent + register: result_localstack + + - name: Try and stop and remove standardnotes web app + community.docker.docker_container: + name: "{{ standardnotes_app_container_name }}" + state: absent + register: result_app + + - name: Check if standardnotes is stopped + ansible.builtin.assert: + that: + - not result.changed + - not result_redis.changed + - not result_db.changed + - not result_localstack.changed + - not result_app.changed diff --git a/roles/standardnotes/tasks/main.yml b/roles/standardnotes/tasks/main.yml new file mode 100644 index 0000000000..9ee818f0be --- /dev/null +++ b/roles/standardnotes/tasks/main.yml @@ -0,0 +1,304 @@ +--- +- name: Start Standard Notes + block: + - name: Create Standard Notes Directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + with_items: + - "{{ standardnotes_data_directory }}" + - "{{ standardnotes_data_directory }}/import" + - "{{ standardnotes_data_directory }}/redis_data" + - "{{ standardnotes_data_directory }}/logs" + - "{{ standardnotes_data_directory }}/mysql" + + - name: Create Standard Notes Upload Directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ standardnotes_user_id | quote }}" + group: "{{ standardnotes_group_id | quote }}" + mode: 0775 + with_items: + - "{{ standardnotes_data_directory }}/uploads" + + - name: Create Standard Notes Network + community.docker.docker_network: + name: "{{ standardnotes_network_name }}" + + - name: Download localstack bootstrap file + ansible.builtin.get_url: + url: https://raw.githubusercontent.com/standardnotes/server/main/docker/localstack_bootstrap.sh + dest: "{{ standardnotes_data_directory }}/localstack_bootstrap.sh" + owner: "{{ standardnotes_user_id | quote }}" + group: "{{ standardnotes_group_id | quote }}" + mode: '0755' + + - name: Create Standard Notes Localstack Container + community.docker.docker_container: + container_default_behavior: no_defaults + name: "{{ standardnotes_localstack_container_name }}" + image: "{{ standardnotes_localstack_image_name }}:{{ standardnotes_localstack_image_version }}" + pull: true + networks: + - name: "{{ standardnotes_network_name }}" + network_mode: "{{ standardnotes_network_name }}" + env: + SERVICES: "sns,sqs" + HOSTNAME_EXTERNAL: "localstack" + LS_LOG: "warn" + exposed_ports: + - 4566 + volumes: + - "{{ standardnotes_data_directory }}/localstack_bootstrap.sh:/etc/localstack/init/ready.d/localstack_bootstrap.sh" + labels: + traefik.enable: "false" + restart_policy: always + memory: "{{ standardnotes_localstack_memory }}" + healthcheck: + test: curl -s http://localhost:4566/_localstack/health + timeout: 10s + interval: 5s + start_period: 60s + + - name: Create Standard Notes Db Container + community.docker.docker_container: + container_default_behavior: no_defaults + name: "{{ standardnotes_db_container_name }}" + image: "{{ standardnotes_db_image_name }}:{{ standardnotes_db_image_version }}" + pull: true + networks: + - name: "{{ standardnotes_network_name }}" + network_mode: "{{ standardnotes_network_name }}" + exposed_ports: + - 3306 + volumes: + - "{{ standardnotes_data_directory }}/mysql:/var/lib/mysql" + - "{{ standardnotes_data_directory }}/import:/docker-entrypoint-initdb.d" + env: + # LANG: "{{ standardnotes_db_lang }}" + MYSQL_DATABASE: "{{ standardnotes_db_database }}" + MYSQL_USER: "{{ standardnotes_db_user }}" + MYSQL_ROOT_PASSWORD: "{{ standardnotes_db_root_password }}" + MYSQL_PASSWORD: "{{ standardnotes_db_password }}" + # MYSQL_INITDB_CHARSET: "{{ standardnotes_db_charset }}" + command: --default-authentication-plugin=caching_sha2_password --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci + labels: + traefik.enable: "false" + restart_policy: unless-stopped + memory: "{{ standardnotes_db_memory }}" + # user: "{{ standardnotes_user_id | quote }}:{{ standardnotes_group_id | quote }}" + healthcheck: + test: [ + "CMD", + "mysqladmin", + "ping", + "-h", "localhost", + '-u', 'root', + '-p{{ standardnotes_db_root_password }}' + ] + timeout: 20s + retries: 10 + start_period: 10s + + - name: Create Standard Notes Redis Container + community.docker.docker_container: + container_default_behavior: no_defaults + name: "{{ standardnotes_redis_container_name }}" + image: "{{ standardnotes_redis_image_name }}:{{ standardnotes_redis_image_version }}" + pull: true + networks: + - name: "{{ standardnotes_network_name }}" + network_mode: "{{ standardnotes_network_name }}" + exposed_ports: + - 6379 + volumes: + - "{{ standardnotes_data_directory }}/redis_data:/data" + labels: + traefik.enable: "false" + restart_policy: always + memory: "{{ standardnotes_redis_memory }}" + user: "{{ standardnotes_user_id | quote }}:{{ standardnotes_group_id | quote }}" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 20s + timeout: 3s + + - name: Create Standard Notes Docker Container + community.docker.docker_container: + container_default_behavior: no_defaults + name: "{{ standardnotes_container_name }}" + image: "{{ standardnotes_image_name }}:{{ standardnotes_image_version }}" + pull: true + networks: + - name: "{{ standardnotes_network_name }}" + network_mode: "{{ standardnotes_network_name }}" + volumes: + - "{{ standardnotes_data_directory }}/logs:/var/lib/server/logs" + - "{{ standardnotes_data_directory }}/uploads:/opt/server/packages/files/dist/uploads" + ports: + - "{{ standardnotes_port }}:3000" + - "{{ standardnotes_files_port }}:3104" + env: + TZ: "{{ ansible_nas_timezone }}" + PUID: "{{ standardnotes_user_id | quote }}" + PGID: "{{ standardnotes_group_id | quote }}" + ###### + # DB # + ###### + + DB_HOST: "{{ standardnotes_db_host }}" + DB_PORT: "{{ standardnotes_db_port }}" + DB_USERNAME: "{{ standardnotes_db_user }}" + DB_PASSWORD: "{{ standardnotes_db_password }}" + DB_DATABASE: "{{ standardnotes_db_database }}" + DB_TYPE: "{{ standardnotes_db_type }}" + + ######### + # CACHE # + ######### + + REDIS_PORT: "{{ standardnotes_redis_port }}" + REDIS_HOST: "{{ standardnotes_redis_host }}" + CACHE_TYPE: "{{ standardnotes_cache_type }}" + + ######## + # KEYS # + ######## + + AUTH_JWT_SECRET: "{{ standardnotes_jwt_secret }}" + AUTH_SERVER_ENCRYPTION_SERVER_KEY: "{{ standardnotes_encryption_server_key }}" + VALET_TOKEN_SECRET: "{{ standardnotes_valet_token_secret }}" + + AUTH_SERVER_SNS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + AUTH_SERVER_SQS_QUEUE_URL: "http://{{ standardnotes_localstack_container_name }}:4566/000000000000/auth-local-queue" + AUTH_SERVER_SQS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + SYNCING_SERVER_SNS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + SYNCING_SERVER_SQS_QUEUE_URL: "http://{{ standardnotes_localstack_container_name }}:4566/000000000000/syncing-server-local-queue" + SYNCING_SERVER_SQS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + FILES_SERVER_SNS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + FILES_SERVER_SQS_QUEUE_URL: "http://{{ standardnotes_localstack_container_name }}:4566/000000000000/files-local-queue" + FILES_SERVER_SQS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + REVISIONS_SERVER_SNS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + REVISIONS_SERVER_SQS_QUEUE_URL: "http://{{ standardnotes_localstack_container_name }}:4566/000000000000/revisions-server-local-queue" + REVISIONS_SERVER_SQS_ENDPOINT: "http://{{ standardnotes_localstack_container_name }}:4566" + + PUBLIC_FILES_SERVER_URL: "{{ standardnotes_public_files_server_url }}" + + restart_policy: unless-stopped + memory: "{{ standardnotes_memory }}" + labels: + traefik.enable: "{{ standardnotes_available_externally | string }}" + traefik.http.routers.standardnotes-server.rule: "Host(`{{ standardnotes_server_hostname }}.{{ ansible_nas_domain }}`)" + traefik.http.routers.standardnotes-server.service: "standardnotes-server" + # traefik.http.routers.standardnotes-server.tls.certresolver: "letsencrypt" + # traefik.http.routers.standardnotes-server.tls.domains[0].main: "{{ ansible_nas_domain }}" + # traefik.http.routers.standardnotes-server.tls.domains[0].sans: "*.{{ ansible_nas_domain }}" + traefik.http.services.standardnotes-server.loadbalancer.server.port: "3000" + traefik.http.routers.standardnotes-files.rule: "Host(`{{ standardnotes_files_hostname }}.{{ ansible_nas_domain }}`)" + traefik.http.routers.standardnotes-files.service: "standardnotes-files" + # traefik.http.routers.standardnotes-files.tls.certresolver: "letsencrypt" + # traefik.http.routers.standardnotes-files.tls.domains[0].main: "{{ ansible_nas_domain }}" + # traefik.http.routers.standardnotes-files.tls.domains[0].sans: "*.{{ ansible_nas_domain }}" + traefik.http.services.standardnotes-files.loadbalancer.server.port: "3104" + # user: "{{ standardnotes_user_id | quote }}:{{ standardnotes_group_id | quote }}" + healthcheck: + test: curl -s http://localhost:3000 + timeout: 10s + interval: 5s + start_period: 60s + + - name: Install Web Client + block: + - name: Create Standard Notes Web Client + community.docker.docker_container: + container_default_behavior: no_defaults + name: "{{ standardnotes_app_container_name }}" + image: "{{ standardnotes_app_image_name }}:{{ standardnotes_app_image_version }}" + pull: true + networks: + - name: "{{ standardnotes_network_name }}" + network_mode: "{{ standardnotes_network_name }}" + volumes: + - "{{ standardnotes_data_directory }}/logs:/var/lib/server/logs" + - "{{ standardnotes_data_directory }}/uploads:/opt/server/packages/files/dist/uploads" + ports: + - "{{ standardnotes_app_port }}:80" + env: + TZ: "{{ ansible_nas_timezone }}" + PUID: "{{ standardnotes_user_id | quote }}" + PGID: "{{ standardnotes_group_id | quote }}" + APP_HOST: "{{ standardnotes_app_host }}" + SF_DEFAULT_SERVER: "{{ standardnotes_sf_default_server }}" + PORT: "{{ standardnotes_app_env_port }}" + + DEFAULT_SYNC_SERVER: "{{ standardnotes_sf_default_server }}" + + # Subscription related endpoints + DASHBOARD_URL: "{{ standardnotes_app_dashboard_url }}" + PLANS_URL: "{{ standardnotes_app_plans_url }}" + PURCHASE_URL: "{{ standardnotes_app_purchase_url }}" + + restart_policy: unless-stopped + memory: "{{ standardnotes_app_memory }}" + labels: + traefik.enable: "{{ standardnotes_available_externally | string }}" + traefik.http.routers.standardnotes-app.rule: "Host(`{{ standardnotes_app_hostname }}.{{ ansible_nas_domain }}`)" + # traefik.http.routers.standardnotes-app.tls.certresolver: "letsencrypt" + # traefik.http.routers.standardnotes-app.tls.domains[0].main: "{{ ansible_nas_domain }}" + # traefik.http.routers.standardnotes-app.tls.domains[0].sans: "*.{{ ansible_nas_domain }}" + traefik.http.services.standardnotes-app.loadbalancer.server.port: "80" + healthcheck: + test: curl -s http://localhost + timeout: 10s + interval: 5s + + when: standardnotes_app_client_enabled is true + + - name: Enable subscription + block: + - name: Check if db container exists and is running + ansible.builtin.command: "docker inspect --format=\"{{ '{{' }} .State.Running {{ '}}' }}\" {{ standardnotes_db_container_name }}" + register: container_running + changed_when: false + ignore_errors: true + + - name: Update user roles + ansible.builtin.command: "docker exec {{ standardnotes_db_container_name }} sh -c \"MYSQL_PWD={{ standardnotes_db_root_password }} mysql {{ standardnotes_db_database }} -e 'INSERT INTO user_roles (role_uuid , user_uuid) VALUES ((SELECT uuid FROM roles WHERE name=\\\"PRO_USER\\\" ORDER BY version DESC limit 1) ,(SELECT uuid FROM users WHERE email=\\\"{{ standardnotes_subscription_email }}\\\")) ON DUPLICATE KEY UPDATE role_uuid = VALUES(role_uuid);'\"" + changed_when: false + when: container_running.stdout == "true" + + - name: Update user subscriptions + ansible.builtin.command: "docker exec {{ standardnotes_db_container_name }} sh -c \"MYSQL_PWD={{ standardnotes_db_root_password }} mysql {{ standardnotes_db_database }} -e 'INSERT INTO user_subscriptions SET uuid=UUID(), plan_name=\\\"PRO_PLAN\\\", ends_at=8640000000000000, created_at=0, updated_at=0, user_uuid=(SELECT uuid FROM users WHERE email=\\\"{{ standardnotes_subscription_email }}\\\"), subscription_id=1, subscription_type=\\\"regular\\\";'\"" + changed_when: false + when: container_running.stdout == "true" + + when: standardnotes_enable_subscription is true + when: standardnotes_enabled is true + +- name: Stop Standard Notes + block: + - name: Stop Standard Notes + community.docker.docker_container: + name: "{{ standardnotes_container_name }}" + state: absent + - name: Stop Standard Notes Redis + community.docker.docker_container: + name: "{{ standardnotes_redis_container_name }}" + state: absent + - name: Stop Standard Notes Db + community.docker.docker_container: + name: "{{ standardnotes_db_container_name }}" + state: absent + - name: Stop Standard Notes Localstack + community.docker.docker_container: + name: "{{ standardnotes_localstack_container_name }}" + state: absent + - name: Uninstall Web Client + block: + - name: Delete Web Client Container + community.docker.docker_container: + name: "{{ standardnotes_app_container_name }}" + state: absent + when: standardnotes_app_client_enabled is true + when: standardnotes_enabled is false diff --git a/website/docs/applications/content-management/standardnotes.md b/website/docs/applications/content-management/standardnotes.md new file mode 100644 index 0000000000..0ed9831c5f --- /dev/null +++ b/website/docs/applications/content-management/standardnotes.md @@ -0,0 +1,18 @@ +--- +title: "Standard Notes" +description: "A free, secure note-taking app with powerful end-to-end encryption" +--- + +Homepage: + +Standard Notes is a free, secure note-taking app with powerful end-to-end encryption, unparalleled privacy features, and seamless cross-platform syncing on unlimited devices. + +## Usage + +Set `standardnotes_enabled: true` in your `inventories//nas.yml` file to install Standard Notes sync server. + +Standard Notes sync server interface can be then found at . + +Optionally, set `standardnotes_app_client_enabled: true` to install Standard Notes Client Web App. + +After server installation and creating of admin user, set `standardnotes_subscription_email` to the admin user email, set `standardnotes_enable_subscription: true` and re-run the playbook to enable subscription features.