diff --git a/charts/function/.helmignore b/charts/function/.helmignore
new file mode 100644
index 0000000..7eb7cb2
--- /dev/null
+++ b/charts/function/.helmignore
@@ -0,0 +1,28 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+
+# OWNERS file for Kubernetes
+OWNERS
+# example production yaml
+values-production.yaml
+# ci files
+ci/
diff --git a/charts/function/Chart.lock b/charts/function/Chart.lock
new file mode 100644
index 0000000..2b49d11
--- /dev/null
+++ b/charts/function/Chart.lock
@@ -0,0 +1,9 @@
+dependencies:
+- name: common
+  repository: https://charts.bitnami.com/bitnami
+  version: 1.11.1
+- name: redis
+  repository: https://charts.bitnami.com/bitnami
+  version: 16.4.0
+digest: sha256:18eac17e69c4871e29365d14fe5822d00dfeb97c5a2624407ad6c506b6542937
+generated: "2022-02-22T00:08:54.6743331+01:00"
diff --git a/charts/function/Chart.yaml b/charts/function/Chart.yaml
new file mode 100644
index 0000000..bcdd311
--- /dev/null
+++ b/charts/function/Chart.yaml
@@ -0,0 +1,44 @@
+apiVersion: v2
+version: 0.0.1
+name: function
+description: Serverless Function
+appVersion: "1.0.0"
+type: application
+keywords:
+  - shortener
+  - url
+  - bitly
+home: https://yourls.org
+icon: https://yourls.org/images/yourls-logo.png
+sources:
+  - https://github.com/YOURLS/function
+  - https://github.com/YOURLS/charts
+maintainers:
+  - name: YOURLS
+    email: yourls@yourls.org
+  - name: LeoColomb
+    email: git@colombaro.fr
+kubeVersion: ">=1.20.0-0"
+dependencies:
+  - name: common
+    repository: https://charts.bitnami.com/bitnami
+    version: ^1.11.1
+    tags:
+      - bitnami-common
+  - condition: redis.enabled
+    name: redis
+    repository: https://charts.bitnami.com/bitnami
+    version: ^16.4.0
+    tags:
+      - function-database
+annotations:
+  artifacthub.io/license: MIT
+  artifacthub.io/links: |
+    - name: Upstream Project
+      url: https://github.com/YOURLS/function
+  artifacthub.io/signKey: |
+    fingerprint: 6FCD850E18E4A4FF76DD1184A3A42A61961E423F
+    url: https://yourls.org/.well-known/openpgpkey/hu/6e4gbtp15q1znf3unweeducn7urty4mr
+  artifacthub.io/changes: |
+    - kind: added
+      description: First version
diff --git a/charts/function/README.md b/charts/function/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/charts/function/templates/_helpers.tpl b/charts/function/templates/_helpers.tpl
new file mode 100644
index 0000000..40c228a
--- /dev/null
+++ b/charts/function/templates/_helpers.tpl
@@ -0,0 +1,102 @@
+{{/* vim: set filetype=mustache: */}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+*/}}
+{{- define "function.redis.fullname" -}}
+{{- include "common.names.dependency.fullname" (dict "chartName" "redis" "chartValues" .Values.redis "context" $) -}}
+{{- end -}}
+
+{{/*
+Return the proper YOURLS image name
+*/}}
+{{- define "function.image" -}}
+{{- $imageRoot := .Values.image -}}
+{{- if not .Values.image.tag }}
+    {{- $tag := (dict "tag" .Chart.AppVersion) -}}
+    {{- $imageRoot := merge .Values.image $tag -}}
+{{- end -}}
+{{- include "common.images.image" (dict "imageRoot" $imageRoot "global" .Values.global) -}}
+{{- end -}}
+
+{{/*
+Return the Function Secret Name
+*/}}
+{{- define "function.secretName" -}}
+{{- if .Values.function.existingSecret }}
+    {{- printf "%s" .Values.function.existingSecret -}}
+{{- else -}}
+    {{- printf "%s" (include "common.names.fullname" .) -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return the Redis™ hostname
+*/}}
+{{- define "function.redisHost" -}}
+{{- if .Values.redis.enabled }}
+    {{- printf "%s-master" (include "function.redis.fullname" .) -}}
+{{- else -}}
+    {{- printf "%s" .Values.externalDatabase.host -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return the Redis™ port
+*/}}
+{{- define "function.redisPort" -}}
+{{- if .Values.redis.enabled }}
+    {{- printf "6379" | quote -}}
+{{- else -}}
+    {{- .Values.externalDatabase.port | quote -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return true if a secret object for Redis™ should be created
+*/}}
+{{- define "function.redis.createSecret" -}}
+{{- if and (not .Values.redis.enabled) (not .Values.externalDatabase.existingSecret) .Values.externalDatabase.password }}
+    {{- true -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return the Redis™ secret name
+*/}}
+{{- define "function.redis.secretName" -}}
+{{- if .Values.redis.enabled }}
+    {{- if .Values.redis.auth.existingSecret }}
+        {{- printf "%s" .Values.redis.auth.existingSecret -}}
+    {{- else -}}
+        {{- printf "%s" (include "function.redis.fullname" .) -}}
+    {{- end -}}
+{{- else if .Values.externalDatabase.existingSecret }}
+    {{- printf "%s" .Values.externalDatabase.existingSecret -}}
+{{- else -}}
+    {{- printf "%s-redis" (include "common.names.fullname" .) -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return the Redis™ secret key
+*/}}
+{{- define "function.redis.secretPasswordKey" -}}
+{{- if and .Values.redis.enabled .Values.redis.auth.existingSecret }}
+    {{- required "You need to provide existingSecretPasswordKey when an existingSecret is specified in redis" .Values.redis.auth.existingSecretPasswordKey | printf "%s" }}
+{{- else if and (not .Values.redis.enabled) .Values.externalDatabase.existingSecret }}
+    {{- required "You need to provide existingSecretPasswordKey when an existingSecret is specified in redis" .Values.externalDatabase.existingSecretPasswordKey | printf "%s" }}
+{{- else -}}
+    {{- printf "redis-password" -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Return whether Redis™ uses password authentication or not
+*/}}
+{{- define "function.redis.auth.enabled" -}}
+{{- if or (and .Values.redis.enabled .Values.redis.auth.enabled) (and (not .Values.redis.enabled) (or .Values.externalDatabase.password .Values.externalDatabase.existingSecret)) }}
+    {{- true -}}
+{{- end -}}
+{{- end -}}
diff --git a/charts/function/templates/configmaps.yaml b/charts/function/templates/configmaps.yaml
new file mode 100644
index 0000000..f68999f
--- /dev/null
+++ b/charts/function/templates/configmaps.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "common.names.fullname" . }}
+  labels: {{- include "common.labels.standard" . | nindent 4 }}
+  {{- if .Values.commonLabels }}
+  {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
+  {{- end }}
+  {{- if .Values.commonAnnotations }}
+  annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
+  {{- end }}
+data:
+  REDIS_HOST: {{ template "function.redisHost" . }}
+  REDIS_PORT_NUMBER: {{ template "function.redisPort" . }}
diff --git a/charts/function/templates/externaldb-secrets.yaml b/charts/function/templates/externaldb-secrets.yaml
new file mode 100644
index 0000000..87fa488
--- /dev/null
+++ b/charts/function/templates/externaldb-secrets.yaml
@@ -0,0 +1,16 @@
+{{- if (include "function.redis.createSecret" .) }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ template "common.names.fullname" . }}-redis
+  labels: {{- include "common.labels.standard" . | nindent 4 }}
+  {{- if .Values.commonLabels }}
+  {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
+  {{- end }}
+  {{- if .Values.commonAnnotations }}
+  annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
+  {{- end }}
+type: Opaque
+data:
+  redis-password: {{ .Values.externalDatabase.password | b64enc | quote }}
+{{- end }}
diff --git a/charts/function/templates/secrets.yaml b/charts/function/templates/secrets.yaml
new file mode 100644
index 0000000..dac590f
--- /dev/null
+++ b/charts/function/templates/secrets.yaml
@@ -0,0 +1,18 @@
+{{- if not .Values.function.existingSecret }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ template "common.names.fullname" . }}
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "common.labels.standard" . | nindent 4 }}
+    {{- if .Values.commonLabels }}
+    {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
+    {{- end }}
+  {{- if .Values.commonAnnotations }}
+  annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
+  {{- end }}
+type: Opaque
+data:
+  endpoint: {{ .Values.function.endpoint | b64enc | quote }}
+  token: {{ .Values.function.token | b64enc | quote }}
+{{- end }}
diff --git a/charts/function/templates/service.yaml b/charts/function/templates/service.yaml
new file mode 100644
index 0000000..070cc26
--- /dev/null
+++ b/charts/function/templates/service.yaml
@@ -0,0 +1,50 @@
+apiVersion: serving.knative.dev/v1
+kind: Service
+metadata:
+  name: {{ include "common.names.fullname" . }}
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "common.labels.standard" . | nindent 4 }}
+    {{- if .Values.commonLabels }}
+    {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
+    {{- end }}
+  {{- if .Values.commonAnnotations }}
+  annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
+  {{- end }}
+spec:
+  template:
+    metadata:
+      labels: {{- include "common.labels.standard" . | nindent 8 }}
+        {{- if .Values.podLabels }}
+        {{- include "common.tplvalues.render" (dict "value" .Values.podLabels "context" $) | nindent 8 }}
+        {{- end }}
+      {{- if or .Values.podAnnotations }}
+      annotations:
+        {{- if .Values.podAnnotations }}
+        {{- include "common.tplvalues.render" (dict "value" .Values.podAnnotations "context" $) | nindent 8 }}
+        {{- end }}
+      {{- end }}
+    spec:
+      containers:
+        - name: function
+          image: {{ template "function.image" . }}
+          env:
+            {{- if (include "function.redis.auth.enabled" .) }}
+            - name: REDIS_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: {{ include "function.redis.secretName" . }}
+                  key: {{ include "function.redis.secretPasswordKey" . }}
+            {{- end }}
+            - name: YOURLS_ENDPOINT
+              valueFrom:
+                secretKeyRef:
+                  name: {{ include "function.secretName" . }}
+                  key: endpoint
+            - name: YOURLS_TOKEN
+              valueFrom:
+                secretKeyRef:
+                  name: {{ include "function.secretName" . }}
+                  key: token
+          envFrom:
+            - configMapRef:
+                name: {{ include "common.names.fullname" . }}
diff --git a/charts/function/values.schema.json b/charts/function/values.schema.json
new file mode 100644
index 0000000..edb0c9d
--- /dev/null
+++ b/charts/function/values.schema.json
@@ -0,0 +1,283 @@
+{
+  "$schema": "https://json-schema.org/schema#",
+  "title": "YOURLS Function Chart JSON Schema",
+  "type": "object",
+  "$defs": {
+    "image": {
+      "title": "Container image description",
+      "type": "object",
+      "properties": {
+        "pullPolicy": {
+          "title": "Specify a imagePullPolicy",
+          "description": "Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'",
+          "type": "string",
+          "enum": [
+            "IfNotPresent",
+            "Always",
+            "Never"
+          ]
+        },
+        "pullSecrets": {
+          "title": "Optionally specify an array of imagePullSecrets",
+          "description": "https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/",
+          "type": "array"
+        },
+        "registry": {
+          "type": "string"
+        },
+        "repository": {
+          "type": "string"
+        },
+        "tag": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "repository",
+        "pullPolicy"
+      ]
+    },
+    "probe": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean"
+        },
+        "failureThreshold": {
+          "type": "integer"
+        },
+        "httpGet": {
+          "type": "object",
+          "properties": {
+            "httpHeaders": {
+              "type": "array"
+            },
+            "path": {
+              "type": "string"
+            },
+            "port": {
+              "type": "string"
+            },
+            "scheme": {
+              "type": "string"
+            }
+          }
+        },
+        "initialDelaySeconds": {
+          "type": "integer"
+        },
+        "periodSeconds": {
+          "type": "integer"
+        },
+        "successThreshold": {
+          "type": "integer"
+        },
+        "timeoutSeconds": {
+          "type": "integer"
+        }
+      }
+    },
+    "resources": {
+      "type": "object",
+      "title": "Required Resources",
+      "description": "Configure resource requests",
+      "form": true,
+      "properties": {
+        "requests": {
+          "type": "object",
+          "properties": {
+            "memory": {
+              "type": "string",
+              "form": true,
+              "render": "slider",
+              "title": "Memory Request",
+              "sliderMin": 10,
+              "sliderMax": 2048,
+              "sliderUnit": "Mi"
+            },
+            "cpu": {
+              "type": "string",
+              "form": true,
+              "render": "slider",
+              "title": "CPU Request",
+              "sliderMin": 10,
+              "sliderMax": 2000,
+              "sliderUnit": "m"
+            }
+          }
+        },
+        "limits": {
+          "type": "object",
+          "properties": {
+            "memory": {
+              "type": "string",
+              "form": true,
+              "render": "slider",
+              "title": "Memory Request",
+              "sliderMin": 10,
+              "sliderMax": 2048,
+              "sliderUnit": "Mi"
+            },
+            "cpu": {
+              "type": "string",
+              "form": true,
+              "render": "slider",
+              "title": "CPU Request",
+              "sliderMin": 10,
+              "sliderMax": 2000,
+              "sliderUnit": "m"
+            }
+          }
+        }
+      }
+    }
+  },
+  "properties": {
+    "function": {
+      "type": "object",
+      "title": "Function Configuration parameters",
+      "description": "https://github.com/YOURLS/function",
+      "form": true,
+      "properties": {
+        "endpoint": {
+          "type": "string",
+          "title": "YOURLS API endpoint name for the function",
+          "examples": [
+            "https://exemple.com"
+          ],
+          "form": true
+        },
+        "token": {
+          "type": "string",
+          "title": "YOURLS API token for the function",
+          "form": true
+        },
+        "existingSecret": {
+          "type": "string"
+        }
+      }
+    },
+    "redis": {
+      "title": "Redis chart configuration",
+      "description": "https://artifacthub.io/packages/helm/bitnami/redis",
+      "allOf": [
+        {
+          "$ref": "https://raw.githubusercontent.com/bitnami/charts/master/bitnami/redis/values.schema.json"
+        },
+        {
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": true,
+              "description": "Deploy a Redis server to satisfy the application database"
+            }
+          }
+        }
+      ]
+    },
+    "externalDatabase": {
+      "type": "object",
+      "title": "External Database Details",
+      "description": "If Redis is disabled. Use this section to specify the external database details",
+      "form": true,
+      "properties": {
+        "host": {
+          "type": "string",
+          "form": true,
+          "title": "Database Host",
+          "hidden": "redis/enabled"
+        },
+        "password": {
+          "type": "string",
+          "form": true,
+          "title": "Database Password",
+          "hidden": "redis/enabled"
+        },
+        "port": {
+          "type": "integer",
+          "form": true,
+          "title": "Database Port",
+          "hidden": "redis/enabled"
+        },
+        "existingSecret": {
+          "type": "string"
+        }
+      }
+    },
+    "commonAnnotations": {
+      "type": "object"
+    },
+    "commonLabels": {
+      "type": "object"
+    },
+    "diagnosticMode": {
+      "type": "object",
+      "properties": {
+        "args": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "command": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "type": "boolean"
+        }
+      }
+    },
+    "extraDeploy": {
+      "type": "array"
+    },
+    "fullnameOverride": {
+      "type": "string"
+    },
+    "global": {
+      "type": "object",
+      "properties": {
+        "imagePullSecrets": {
+          "type": "array"
+        },
+        "imageRegistry": {
+          "type": "string"
+        },
+        "storageClass": {
+          "type": "string"
+        }
+      }
+    },
+    "image": {
+      "title": "Function container image",
+      "allOf": [
+        {
+          "$ref": "#/$defs/image"
+        },
+        {
+          "properties": {
+            "debug": {
+              "title": "Enable image debug mode",
+              "default": false,
+              "type": "boolean"
+            }
+          }
+        }
+      ]
+    },
+    "kubeVersion": {
+      "type": "string"
+    },
+    "nameOverride": {
+      "type": "string"
+    },
+    "podAnnotations": {
+      "type": "object"
+    },
+    "podLabels": {
+      "type": "object"
+    }
+  }
+}
diff --git a/charts/function/values.yaml b/charts/function/values.yaml
new file mode 100644
index 0000000..3c63c14
--- /dev/null
+++ b/charts/function/values.yaml
@@ -0,0 +1,154 @@
+## @section Global parameters
+## Global Docker image parameters
+## Please, note that this will override the image parameters, including dependencies, configured to use the global value
+## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass
+##
+
+## @param global.imageRegistry Global Docker image registry
+## @param global.imagePullSecrets Global Docker registry secret names as an array
+## @param global.storageClass Global StorageClass for Persistent Volume(s)
+##
+global:
+  imageRegistry: ""
+  ## E.g.
+  ## imagePullSecrets:
+  ##   - myRegistryKeySecretName
+  ##
+  imagePullSecrets: []
+  storageClass: ""
+
+## @section Common parameters
+##
+
+## @param kubeVersion Override Kubernetes version
+##
+kubeVersion: ""
+## @param nameOverride String to partially override common.names.fullname
+##
+nameOverride: ""
+## @param fullnameOverride String to fully override common.names.fullname
+##
+fullnameOverride: ""
+## @param commonLabels Labels to add to all deployed objects
+##
+commonLabels: {}
+## @param commonAnnotations Annotations to add to all deployed objects
+##
+commonAnnotations: {}
+## @param clusterDomain Kubernetes cluster domain name
+##
+clusterDomain: cluster.local
+## @param extraDeploy Array of extra objects to deploy with the release
+##
+extraDeploy: []
+
+## Enable diagnostic mode in the deployment
+##
+diagnosticMode:
+  ## @param diagnosticMode.enabled Enable diagnostic mode (all probes will be disabled and the command will be overridden)
+  ##
+  enabled: false
+  ## @param diagnosticMode.command Command to override all containers in the deployment
+  ##
+  command:
+    - sleep
+  ## @param diagnosticMode.args Args to override all containers in the deployment
+  ##
+  args:
+    - infinity
+
+## @section Function container image
+##
+
+## @param image.registry Image registry
+## @param image.repository Image repository
+## @param image.tag Image tag (immutable tags are recommended)
+## @param image.pullPolicy Image pull policy
+## @param image.pullSecrets Image pull secrets
+## @param image.debug Enable image debug mode
+##
+image:
+  registry: ghcr.io
+  repository: yourls/function
+  ## Defaults to '{{ .Chart.AppVersion }}'
+  ##
+  tag: ""
+  ## Specify a imagePullPolicy
+  ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
+  ## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
+  ##
+  pullPolicy: Always
+  ## Optionally specify an array of imagePullSecrets.
+  ## Secrets must be manually created in the namespace.
+  ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+  ## e.g:
+  ## pullSecrets:
+  ##   - myRegistryKeySecretName
+  ##
+  pullSecrets: []
+  ## Enable debug mode
+  ##
+  debug: false
+
+## @section Function parameters
+##
+function:
+  endpoint: ""
+  token: ""
+  existingSecret: ""
+
+## @param podAnnotations Additional pod annotations
+## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+##
+podAnnotations: {}
+## @param podLabels Additional pod labels
+## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+##
+podLabels: {}
+
+## @section Redis™ parameters
+
+## Redis™ chart configuration
+## https://github.com/bitnami/charts/blob/master/bitnami/redis/values.yaml
+##
+redis:
+  ## @param redis.enabled Whether to deploy a redis server to satisfy the applications requirements. To use an external redis instance set this to false and configure the externalRedis parameters
+  ##
+  enabled: true
+  ## Use password authentication
+  ## @param redis.auth.enabled Use password authentication
+  ## @param redis.auth.password Redis™ password (both master and replica)
+  ## @param redis.auth.existingSecret Name of an existing Kubernetes secret object containing the password
+  ## @param redis.auth.existingSecretPasswordKey Name of the key pointing to the password in your Kubernetes secret
+  ##
+  auth:
+    enabled: false
+    ## Defaults to a random 10-character alphanumeric string if not set and auth.enabled is true.
+    ## It should always be set using the password value or in the existingSecret to avoid issues
+    ## with Discourse.
+    ## The password value is ignored if existingSecret is set
+    password: ""
+    existingSecret: ""
+    existingSecretPasswordKey: 'redis-password'
+  ## @param redis.architecture Cluster settings
+  ##
+  architecture: standalone
+  ## Redis™ Master parameters
+  ## @param redis.master.persistence.enabled Enable database persistence using PVC
+  ##
+  master:
+    persistence:
+      enabled: true
+## External Redis™
+## @param externalRedis.host Host of the external database
+## @param externalRedis.port Database port number
+## @param externalRedis.password Password for the external Redis. Ignored if existingSecret is set
+## @param externalRedis.existingSecret Name of an existing Kubernetes secret object containing the password
+## @param externalRedis.existingSecretPasswordKey Name of the key pointing to the password in your Kubernetes secret
+##
+externalDatabase:
+  host: ""
+  port: 6379
+  password: ""
+  existingSecret: ""
+  existingSecretPasswordKey: 'redis-password'