diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4f107b738b85ac57da82e59876f568b89d0846b2..353faa1e8f60b234287c1b52be74607530faeddd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -173,6 +173,17 @@ include:
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-wordpress/'
     - if: '$CI_COMMIT_BRANCH == "master"'
 
+.zulip_rules:
+  rules:
+    - changes:
+        - flux2/apps/$RESOURCE/*.yaml
+        - flux2/cluster/optional/$RESOURCE/*.yaml
+        - flux2/infrastructure/sources/zulip.yaml
+        - install/install-app.sh
+        - test/taiko/*
+    - if: '$TRIGGER_JOBS =~ /enable-zulip/'
+    - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-zulip/'
+    - if: '$CI_COMMIT_BRANCH == "master"'
 
 # Global declarations
 # ===================
@@ -436,6 +447,13 @@ enable-wordpress:
     - .enable_app_template
     - .wordpress_rules
 
+enable-zulip:
+  variables:
+    RESOURCE: "zulip"
+  extends:
+    - .enable_app_template
+    - .zulip_rules
+
 # Stage: apps-kustomizations-ready
 # ================
 #
@@ -496,6 +514,16 @@ wordpress-kustomization-ready:
     - .app-kustomization-ready
     - .wordpress_rules
 
+zulip-kustomization-ready:
+  needs:
+    - job: setup-openappstack
+    - job: enable-zulip
+  variables:
+    RESOURCE: "zulip"
+  extends:
+    - .app-kustomization-ready
+    - .zulip_rules
+
 # Stage: certs
 # ================
 #
@@ -571,6 +599,16 @@ wordpress-cert:
     - .apps-cert
     - .wordpress_rules
 
+zulip-cert:
+  variables:
+    RESOURCE: "zulip"
+  needs:
+    - job: enable-zulip
+    - job: setup-openappstack
+  extends:
+    - .apps-cert
+    - .zulip_rules
+
 
 # Stage: health-test
 # ==================
@@ -692,6 +730,17 @@ wordpress-taiko:
     - .taiko
     - .wordpress_rules
 
+zulip-taiko:
+  variables:
+    RESOURCE: "zulip"
+  needs:
+    - job: zulip-cert
+    - job: setup-openappstack
+    - job: zulip-kustomization-ready
+  extends:
+    - .taiko
+    - .zulip_rules
+
 
 # Etc
 # ===
diff --git a/.gitlab/issue_templates/new_app.md b/.gitlab/issue_templates/new_app.md
index ec7b3df755eac697e2adca4494f7b3196420e9d5..32777200c2c66baedd18fa762500cb5e51f316ac 100644
--- a/.gitlab/issue_templates/new_app.md
+++ b/.gitlab/issue_templates/new_app.md
@@ -5,21 +5,21 @@
 
 * [ ] Create new source if needed in `flux2/infrastructure/sources/APP.yaml`
 * [ ] Include `APP.yaml` in `flux2/infrastructure/sources/kustomization.yaml`
-* [ ] Add app secret: `charts/oas-secrets/templates/oas-APP-variables.yaml`
-* Add kustomizations:
+* [ ] Add app secret: `install/templates/oas-APP-variables.yaml.jinja`
+* Add `Kustomizations`:
    * [ ] `flux2/cluster/optional/APP/APP.yaml`
    * [ ] `flux2/apps/APP/kustomization.yaml`
    * [ ] If needed, add PVCs in `flux2/apps/APP/pvc.yaml`
-   * [ ] Add helmrelease in `flux2/apps/APP/release.yaml`
+   * [ ] Add `HelmRelease` in `flux2/apps/APP/release.yaml`
 
 ### Single sign-on
 
 * Integrate the new app into the single sign-on system
-  * Add OAuth client secret to `charts/oas-secrets/templates/oas-oauth-variables.yaml`
+  * [ ] Add OAuth client secret to `install/templates/oas-oauth-variables.yaml.jinja`
   * In `flux2/core/base/single-sign-on/release.yaml`:
     * [ ] Add app `userbackend.applications`
     * [ ] Add app to `oAuthClients`
-  * Confgure app OIDC settings in helmrelease `flux2/apps/APP/release.yaml`
+  * [ ] Configure app OIDC settings in `HelmRelease` `flux2/apps/APP/release.yaml`
   * [ ] Disable user/pw login if possible
   * [ ] Admin-login should grant admin privileges
   * [ ] Non-admin should not grant admin privileges
@@ -28,21 +28,21 @@
 
 * [ ] Make sure testing app resources work (`test/pytest/test_resources.py`)
 * [ ] Make sure testing app cert works (`test/pytest/test_certs.py`)
-* [ ] Add taiko test (`tests/taiko`)
+* [ ] Add Taiko test (`tests/taiko`)
 
 ## CI
 
-Add app to following stages in `.gitlab-ci.yml`:
+Add the following elements to `.gitlab-ci.yml`:
 
-* [ ] install-apps
-* [ ] apps-helm-release
-* [ ] apps-ready
-* [ ] certs
-* [ ] integration-tests
+* [ ] `.APP-rules` partial
+* [ ] `enable-APP` job
+* [ ] `APP-kustomization-ready` job
+* [ ] `APP-cert` job
+* [ ] `APP-taiko` test job
 
 ## Renovatebot
 
-* [ ] Make sure the needed helmRelease fields for renovatebot are in place and
+* [ ] Make sure the needed `HelmRelease` fields for renovatebot are in place and
       order, i.e.
       ```
       # renovate: registryUrl=https://helm-charts.wikimedia.org/stable/
diff --git a/flux2/apps/zulip/kustomization.yaml b/flux2/apps/zulip/kustomization.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e034f8f508dc7f5a3228b6693b4f120630511dbf
--- /dev/null
+++ b/flux2/apps/zulip/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: oas-apps
+resources:
+  - release.yaml
+  - zulip-values-configmap.yaml
diff --git a/flux2/apps/zulip/release.yaml b/flux2/apps/zulip/release.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..69c5e4e72e88ad79ab6a7ed55a8f990ee6992d3b
--- /dev/null
+++ b/flux2/apps/zulip/release.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: zulip
+  namespace: oas-apps
+spec:
+  releaseName: zulip
+  chart:
+    spec:
+      chart: zulip
+      # NOTE: Change the GitRepository yaml file if you want a different version
+      sourceRef:
+        kind: GitRepository
+        name: zulip-helm-chart
+        namespace: flux-system
+  interval: 1h
+  install:
+    timeout: 15m
+  valuesFrom:
+    - kind: ConfigMap
+      name: oas-zulip-values
+      optional: false
+    # Allow overriding values by ConfigMap or Secret
+    - kind: ConfigMap
+      name: oas-zulip-override
+      optional: true
+    - kind: Secret
+      name: oas-zulip-override
+      optional: true
diff --git a/flux2/apps/zulip/zulip-values-configmap.yaml b/flux2/apps/zulip/zulip-values-configmap.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..21944a7f60082f10448c990c3dcb9cfce8026baf
--- /dev/null
+++ b/flux2/apps/zulip/zulip-values-configmap.yaml
@@ -0,0 +1,78 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: oas-rocketchat-values
+data:
+  values.yaml: |
+    ingress:
+      hosts:
+        - zulip.${domain}
+      annotations:
+        # Tell cert-manager to automatically get a TLS certificate
+        kubernetes.io/tls-acme: "true"
+      tls:
+        - hosts:
+            - "zulip.${domain}"
+          secretName: oas-zulip
+
+    memcached:
+      password: "${memcached_password}"
+      resources:
+        limits:
+          cpu: 200m
+          memory: 256Mi
+        requests:
+          cpu: 100m
+          memory: 128Mi
+
+    redis:
+      password: "${redis_password}"
+      resources:
+        limits:
+          cpu: 200m
+          memory: 64Mi
+        requests:
+          cpu: 100m
+          memory: 32Mi
+
+    postgresql:
+      password: "${postgresql_password}"
+      resources:
+        limits:
+          cpu: 400m
+          memory: 256Mi
+        requests:
+          cpu: 200m
+          memory: 128Mi
+
+    zulip:
+      image: open.greenhost.net:4567/openappstack/openappstack/zulip:f60b8cc
+      environment:
+        DISABLE_HTTPS: true
+        SSL_CERTIFICATE_GENERATION: self-signed
+        SETTING_EXTERNAL_HOST: zulip.${domain}
+        SETTING_ZULIP_ADMINISTRATOR: "${admin_email}"
+        SECRETS_email_password: "${outgoing_mail_smtp_password}"
+        SETTING_EMAIL_HOST: '${outgoing_mail_smtp_host}'
+        SETTING_EMAIL_HOST_USER: '${outgoing_mail_smtp_user}'
+        SETTING_EMAIL_PORT: '${outgoing_mail_smtp_host}'
+        SETTING_EMAIL_USE_SSL: 'False'
+        SETTING_EMAIL_USE_TLS: 'True'
+        ZULIP_AUTH_BACKENDS: 'EmailAuthBackend'
+        # NOTE: Needs to be a Python Tuple
+        SETTING_AUTHENTICATION_BACKENDS: '("zproject.backends.GenericOpenIdConnectBackend",)'
+        # NOTE: Needs adjusted entrypoint that's currently only in our Docker container
+        # (https://github.com/greenhost/docker-zulip/commit/d583a2d28707a3b77bf610bedc2c2bb81f2a5f88)
+        # NOTE: This is a Python object, not JSON
+        SETTING_SOCIAL_AUTH_OIDC_ENABLED_IDPS: '{"openappstack": { "oidc_url": "https://sso.${domain}/", "display_name": "OpenAppStack", "display_icon": None, "client_id": "zulip", "secret": get_secret("social_auth_oidc_secret"), "auto_signup": True }}'
+        SECRETS_social_auth_oidc_secret: "${zulip_oauth_client_secret}"
+        # Enable "low memory mode", queue workers run 1 multithreaded process
+        QUEUE_WORKERS_MULTIPROCESS: 'False'
+      resources:
+        limits:
+          cpu: 400m
+          memory: 1.5Gi
+        requests:
+          cpu: 100m
+          memory: 1Gi
diff --git a/flux2/cluster/optional/zulip/zulip.yaml b/flux2/cluster/optional/zulip/zulip.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4a602c685d6377538dd23d213b656eb015f85be0
--- /dev/null
+++ b/flux2/cluster/optional/zulip/zulip.yaml
@@ -0,0 +1,50 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
+kind: Kustomization
+metadata:
+  name: zulip
+  namespace: flux-system
+spec:
+  interval: 1h
+  dependsOn:
+    - name: core
+    - name: infrastructure
+  sourceRef:
+    kind: GitRepository
+    name: openappstack
+  path: ./flux2/apps/zulip
+  prune: true
+  validation: client
+  healthChecks:
+    - apiVersion: helm.toolkit.fluxcd.io/v1beta1
+      kind: HelmRelease
+      name: zulip
+      namespace: oas-apps
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: zulip-zulip
+      namespace: oas-apps
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: zulip-postgresql
+      namespace: oas-apps
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: zulip-redis
+      namespace: oas-apps
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: zulip-rabbitmq
+      namespace: oas-apps
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: zulip-zulip
+      namespace: oas-memcached
+  postBuild:
+    substituteFrom:
+      - kind: Secret
+        name: oas-zulip-variables
+      - kind: Secret
+        name: oas-oauth-variables
+      - kind: Secret
+        name: oas-cluster-variables
diff --git a/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml b/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
index 689719a80dbf36ea8b70460599d16f8f522efe11..bcdb908f67f867fada1f367feb7d7dc1e64f265e 100644
--- a/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
+++ b/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
@@ -26,6 +26,8 @@ data:
           description: "Grafana allows you to query, visualize, alert on and understand metrics generated by OpenAppStack. It can be used to create explore and share dashboards."
         - name: &WEKAN wekan
           description: "Wekan Kanban board."
+        - name: &ZULIP zulip
+          description: "Communicate and collaborate using team chat and switch to video or audio calls with screen sharing for more efficient teamwork."
       username: "${userbackend_admin_username}"
       password: "${userbackend_admin_password}"
       email: "${admin_email}"
@@ -150,3 +152,19 @@ data:
         - "refresh_token"
         - "client_credentials"
         - "implicit"
+    # https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect
+    - clientName: *ZULIP
+      clientSecret: "${zulip_oauth_client_secret}"
+      redirectUri: "https://zulip.${domain}/complete/oidc/"
+      scopes: "openid profile email"
+      clientUri: "https://zulip.${domain}"
+      clientLogoUri: "https://zulip.${domain}/static/images/zulip-logo.svg"
+      tokenEndpointAuthMethod: "client_secret_post"
+      responseTypes:
+        - "code"
+        - "id_token"
+      grantTypes:
+        - "authorization_code"
+        - "refresh_token"
+        - "client_credentials"
+        - "implicit"
diff --git a/flux2/infrastructure/sources/kustomization.yaml b/flux2/infrastructure/sources/kustomization.yaml
index 7e832c9b1c7bff90d1488bc36ff97746d4eb2127..c86eee7c82c3247b740e2aa36d3d6ec9e0c962a7 100644
--- a/flux2/infrastructure/sources/kustomization.yaml
+++ b/flux2/infrastructure/sources/kustomization.yaml
@@ -16,3 +16,4 @@ resources:
   - wekan.yaml
   - wikimedia.yaml
   - wordpress.yaml
+  - zulip.yaml
diff --git a/flux2/infrastructure/sources/zulip.yaml b/flux2/infrastructure/sources/zulip.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..42a645bb51d7928ae00ba5fa65683cb39776bc34
--- /dev/null
+++ b/flux2/infrastructure/sources/zulip.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta1
+kind: GitRepository
+metadata:
+  name: zulip-helm-chart
+  namespace: flux-system
+spec:
+  # The interval at which to check the upstream for updates
+  interval: 1h
+  # The repository URL, can be a HTTP/S or SSH address
+  url: https://github.com/greenhost/docker-zulip
+  # The Git reference to checkout and monitor for changes
+  # (defaults to master)
+  # For all available options, see:
+  # https://toolkit.fluxcd.io/components/source/api/#source.toolkit.fluxcd.io/v1beta1.GitRepositoryRef
+  ref:
+    branch: helm-chart-stackspin
diff --git a/install/templates/oas-oauth-variables.yaml.jinja b/install/templates/oas-oauth-variables.yaml.jinja
index befd2f774e40402a86caf20ea51c7173342165f7..95f43833a082fc0f0aea8a841f07dfb8707a374c 100644
--- a/install/templates/oas-oauth-variables.yaml.jinja
+++ b/install/templates/oas-oauth-variables.yaml.jinja
@@ -10,3 +10,4 @@ data:
   userpanel_oauth_client_secret: "{{ 32 | generate_password | b64encode }}"
   wekan_oauth_client_secret: "{{ 32 | generate_password | b64encode }}"
   wordpress_oauth_client_secret: "{{ 32 | generate_password | b64encode }}"
+  zulip_oauth_client_secret: "{{ 32 | generate_password | b64encode }}"
diff --git a/install/templates/oas-zulip-variables.yaml.jinja b/install/templates/oas-zulip-variables.yaml.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..17f59d1157f160a2e32da02b2d050518f759bf64
--- /dev/null
+++ b/install/templates/oas-zulip-variables.yaml.jinja
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: oas-zulip-variables
+data:
+  memcached_password: "{{ 32 | generate_password | b64encode }}"
+  redis_password: "{{ 32 | generate_password | b64encode }}"
+  postgresql_password: "{{ 32 | generate_password | b64encode }}"
diff --git a/test/pytest/test_certs.py b/test/pytest/test_certs.py
index 2240e3f2371d7008a3439773d34d3b2ab30fdfb6..a4c9f4219f48c878ba7ca37b7199d4414fe6b8d0 100755
--- a/test/pytest/test_certs.py
+++ b/test/pytest/test_certs.py
@@ -2,14 +2,16 @@
 """Test if application ingress uses a valid certificate."""
 
 import os
-import socket
 import shutil
+import socket
 import sys
+
 import certifi
 import pytest
 import requests
 from OpenSSL import SSL
 
+
 @pytest.mark.resource
 @pytest.mark.certs
 def test_cert_validation(host, resource): # pylint: disable=too-many-statements
@@ -101,7 +103,8 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
         'rocketchat': 'chat',
         'single-sign-on': 'sso',
         'wekan': 'wekan',
-        'wordpress': 'www'
+        'wordpress': 'www',
+        'zulip': 'zulip',
     }
 
     if resource == 'all':
diff --git a/test/taiko/apps.js b/test/taiko/apps.js
index 5cbbb587e5a9a6d78f1e9e553e4f9211c7e8aeab..7c4edda9280eddd61af9b347e5c7ffb378a75008 100644
--- a/test/taiko/apps.js
+++ b/test/taiko/apps.js
@@ -186,6 +186,15 @@ const assert = require('assert');
       await goto(dashboardUrl)
     }
 
+    // Zulip
+    if (taikoTests.includes('zulip') || taikoTests === 'all') {
+      const dashboardUrl = 'https://zulip.' + domain
+
+      console.log('• Zulip')
+      await goto(zulipUrl)
+      await click("Log in with OpenAppStack")
+    }
+
   } catch (error) {
     await screenshot()
     console.error(error)