@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
1,123 lines (1,021 loc) • 128 kB
Markdown
# govcy Express Services
[](https://www.npmjs.com/package/@gov-cy/govcy-express-services)

[](https://github.com/gov-cy/govcy-express-services/actions/workflows/unit-test.yml)
[](https://github.com/gov-cy/govcy-express-services/actions/workflows/tag-and-publish-on-version-change.yml)
[](coverage-summary.json)
> ⚠️ **Warning:**
> **No guarantees are provided regarding stability, security, or compliance. Using this package does not imply your product or service will automatically pass any required assessments, audits, or certifications by the Cyprus government or any other authority.**
>
> You are responsible for ensuring your own compliance, security, and quality assurance processes.
## 📝 Description
This project is an Express-based project that dynamically renders online service forms using `@gov-cy/govcy-frontend-renderer`, handles data input, validations, renders a review page and submits the data via a submission API. It is designed for developers building government services in Cyprus, enabling them to manage user authentication, form submissions, and OpenID authentication workflows in a timely manner.
The project is designed to support the [Linear structure](https://gov-cy.github.io/govcy-design-system-docs/patterns/service_structure/#variant-1---linear-structure) as described in the [Unified Design System](https://gov-cy.github.io/govcy-design-system-docs/).
The APIs used for submission, temporary save and file uploads are not part of this project. The project has been designed to work together with the **DSF Submission plarform** and all API calls are based on the **DSF Submission Platform APIs**. For more details about the DSF Submission Platform [contact the DSF team](https://dsf.dmrid.gov.cy/contact/).
This readme file describes the definition of these APIs and how to use them. If you wish to develop your own government back-end solution that integrates with Express Services see the [Backend Integration Specification Documentation](docs/Backend-integration-specification.md).

## Table of contents
- [📝 Description](#-description)
- [✨ Features](#-features)
- [📋 Prerequisites](#-prerequisites)
- [🚀 Quick start](#-quick-start)
- [✅ Best Practices](#-best-practices)
- [📦 Full installation guide](#-full-installation-guide)
- [🛠️ Usage](#%EF%B8%8F-usage)
- [🔑 Authentication Middleware](#-authentication-middleware)
- [cyLogin Access Policies](#cylogin-access-policies)
- [🧩 Dynamic services](#-dynamic-services)
- [Pages](#pages)
- [Task list pages](#task-list-pages)
- [Form vs static pages](#form-vs-static-pages)
- [Update my details pages](#update-my-details-pages)
- [Multiple things pages (repeating group of inputs)](#multiple-things-pages-repeating-group-of-inputs)
- [Review page](#review-page)
- [Success page](#success-page)
- [🛡️ Site eligibility checks](#%EF%B8%8F-site-eligibility-checks)
- [📤 Site submissions](#-site-submissions)
- [✅ Input validations](#-input-validations)
- [🔀 Conditional logic](#-conditional-logic)
- [💾 Temporary save feature](#-temporary-save-feature)
- [🗃️ Files uploads feature](#%EF%B8%8F-files-uploads-feature)
- [✨ Custom pages feature](#-custom-pages-feature)
- [🛣️ Routes](#%EF%B8%8F-routes)
- [👨💻 Environment variables](#-environment-variables)
- [🔒 Security note](#-security-note)
- [❓ Troubleshooting / FAQ](#-troubleshooting--faq)
- [🙏 Credits](#-credits)
- [💡 Developer notes](#-developer-notes)
- [📄 License](#-license)
- [📬 Contact](#contact)
## ✨ Features
- Dynamic form rendering from JSON templates
- Support for `textInput`, `textArea`, `select`, `radios`, `checkboxes`, `datePicker`, `dateInput`, `fileInput` elements
- Support for `conditional radios`
- Dynamic creation of check your answers page
- OpenID Connect authentication with CY Login
- Middleware-based architecture for better maintainability
- Supports routing for dynamic pages
- Input validation
- CSRF protection
- cyLogin Single Sign-On (SSO) for physical authorized users
- Pre-filling posted values (in the same session)
- Site level API eligibility checks
- API integration with retry logic for form submissions.
- Optional temporary save of in-progress form data via configurable API endpoints
- Optional file uploads via API endpoints
## 📋 Prerequisites
- Node.js 20+
- npm
- A CY Login client ID and secret
- An API endpoint for form submissions (through cyConnect)
## 🚀 Quick start
```sh
# 1. Install the package
npm install @gov-cy/govcy-express-services
# 2. Generate SSL certificates for local development
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.cert -days 365 -nodes
# 3. Create a `secrets/.env` file in your project (see below for required variables)
# 4. Add a minimal data config file in /data (see test.json example)
# 5. Create an index.mjs file:
```
```js
// index.mjs
import initializeGovCyExpressService from '@gov-cy/govcy-express-services';
const service = initializeGovCyExpressService();
service.startServer();
```
```sh
# 6. Start the server
npm start
```
- Visit [https://localhost:44319](https://localhost:44319) in your browser.
- Log in with CY Login and start using your dynamic service!
---
**Tip:**
For more details on configuration, environment variables, and advanced features, see the sections below.
## 📦 Full installation guide
The project acts as an npm package and you need to install it as a dependency in your npm project. Check out the [install notes](INSTALL-NOTES.md) a detailed installation guide.
## ✅ Best Practices
Before starting your service, please review the [Best Practices guide](BEST-PRACTICES.md) for guidance on:
- Repository structure
- Environment separation (`dev` / `staging` / `prod`)
- Secure CY Login client registration
- Mandatory footer pages (privacy, cookies, accessibility)
## 🛠️ Usage
### Starting the Server
Add in your `package.json`:
```json
"scripts": {
"start": "node index.mjs"
}
```
Then run the server using `npm start`.
```sh
npm start
```
The server will start on `https://localhost:44319` (see [NOTES.md](NOTES.md#local-development) for more details on this).
### 🔑 Authentication Middleware
Authentication is handled via OpenID Connect using CY Login and is configured using environment variables. The middleware ensures users have valid sessions before accessing protected routes.
The CY Login tokens are used to also connect with the various APIs through [cyConnect](https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/74/CY-Connect), so make sure to include the correct `scope` when requesting for a [cyLogin client registration](https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/34/Developer-Guide).
The CY Login settings are configured in the `secrets/.env` file.
#### cyLogin Access Policies
Each service can specify which types of authenticated CY Login profiles are allowed to access it using the `site.cyLoginPolicies` property in its site configuration.
```json
"cyLoginPolicies": ["naturalPerson", "legalPerson"]
```
##### Supported Policies
| Policy name | Description | Typical use |
| --------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
| `naturalPerson` | Allows individual users (Cypriot citizens or foreign residents) who have a verified profile in the Civil Registry. Identified by `profile_type: "Individual"` and a 10-digit identifier starting with `00` (citizen) or `05` (foreigner). | Citizen-facing services, personal applications, etc. |
| `legalPerson` | Allows legal entities (companies, partnerships, organisations) with verified profiles in the Registrar of Companies. Identified by `profile_type: "Organisation"` and a `legal_unique_identifier`. | Business-facing services, company submissions, etc. |
##### How it works
- Access is granted if **any** of the listed policies pass.
- If the user’s CY Login profile does not match any of the allowed policies, the request is blocked.
##### Defaults
If `cyLoginPolicies` is omitted, the framework defaults to:
```json
"cyLoginPolicies": ["naturalPerson"]
```
This maintains backward compatibility with existing services that only supported individual (civil registry) users.
##### Example
Allow both natural and legal persons:
```json
"site": {
"cyLoginPolicies": ["naturalPerson", "legalPerson"]
}
```
Restrict access to natural persons only:
```json
"site": {
"cyLoginPolicies": ["naturalPerson"]
}
```
##### Notes
- This configuration applies globally to the service.
- Both `requireAuth` and `cyLoginPolicy` middlewares must be present on protected routes (automatically included by the default route setup).
### 🧩 Dynamic Services
Services are rendered dynamically using JSON templates stored in the `/data` folder. All the service configuration, pages, routes, and logic is stored in the JSON files. The service will load `data/:siteId.json` to get the form data when a user visits `/:siteId/:pageUrl`. Checkout the [express-service-shema.json](express-service-shema.json) and the example JSON structure of the **[test.json](data/test.json)** file for more details.
Here is an example JSON config:
```json
{
"site": {
"id": "test",
"usesDSFSubmissionPlatform": true, //<-- Indicates whether the service uses the DSF submission platform (transforms submission data as needed)
"cyLoginPolicies": ["naturalPerson"], //<-- Allowed CY Login policies
"lang": "el", //<-- Default language
"languages": [ //<-- Supported languages
{
"code": "el",
"label": "EL",
"alt": "Ελληνική γλώσσα",
"href": "?lang=el"
},
{
"code": "en",
"label": "EN",
"alt": "English language",
"href": "?lang=en"
}
],
"footerLinks": [ //<-- Links on the footer
{
"label": {
"el": "Δήλωση απορρήτου",
"en": "Privacy statement",
"tr": "Privacy statement"
},
"href": "test/privacy-statement"
},
{
"label": {
"el": "Cookies",
"en": "Cookies",
"tr": "Cookies"
},
"href": "test/cookie-policy"
},
{
"label": {
"el": "Προσβασιμότητα",
"en": "Accessibility",
"tr": "Accessibility"
},
"href": "test/accessibility-statement"
}
],
"footerIcons": [ //<-- Icons on the footer
{
"target": "_blank",
"src": {
"el": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/FundedbyEU_NextGeneration_H53-EL.png",
"en": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/FundedbyEU_NextGeneration_H53-EN.png",
"tr": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/FundedbyEU_NextGeneration_H53-EN.png"
},
"alt": {
"el": "Χρηματοδοτείται από την ΕΕ Next Generation EU",
"en": "Funded by the EU Next Generation EU",
"tr": "Funded by the EU Next Generation EU"
},
"href": {
"el": "https://europa.eu/",
"en": "https://europa.eu/",
"tr": "https://europa.eu/"
},
"title": {
"el": "Μετάβαση στην ιστοσελίδα της ΕΕ",
"en": "Go to EU website",
"tr": "Go to EU website"
}
},
{
"target": "_blank",
"src": {
"el": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/CYpros%20to%20aurio%20logo%20eng_H53_EL.png",
"en": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/CYpros%20to%20aurio%20logo%20eng_H53_EN.png",
"tr": "https://cdn.jsdelivr.net/gh/gov-cy/govdesign@main/CYpros%20to%20aurio%20logo%20eng_H53_EN.png"
},
"alt": {
"el": "Κύπρος το Αύριο, σχέδιο ανάκαμψης και ανθεντικότητας",
"en": "Cyprus tomorrow, recovery and resilience plan",
"tr": "Cyprus tomorrow, recovery and resilience plan"
},
"href": {
"el": "http://www.cyprus-tomorrow.gov.cy/",
"en": "http://www.cyprus-tomorrow.gov.cy/",
"tr": "http://www.cyprus-tomorrow.gov.cy/"
},
"title": {
"el": "Μετάβαση στην ιστοσελίδα Κύπρος το Αύριο",
"en": "Go to Cyprus Tomorrow website",
"tr": "Go to Cyprus Tomorrow website"
}
}
],
"menu": { //<-- Menu altext
"el": "Μενού",
"en": "Menu",
"tr": "Menu"
},
"title": { //<-- Service title (meta)
"el": "Υπηρεσία τεστ",
"en": "Test service",
"tr": ""
},
"headerTitle": { // <-- The header title settings
"title": { //<-- Service title (as it apears in the header)
"el": "[Το ΟΝΟΜΑ της υπηρεσίας που θα φαίνεται στις φόρμες]",
"en": "[The NAME of the service as it will appear on forms]",
"tr": ""
},
"href": { // <-- The relative URL of the header title link (for each language)
"el":"/service-id",
"en":"/service-id",
"tr":"/service-id"
}
},
"reviewPageHeader": { //<-- OPTIONAL - Review page header. Useful when directing users to the review page from gov.cy start page
"el": "Υπηρεσία τεστ",
"en": "Test service",
"tr": "Test service"
},
"successPageHeader": { //<-- OPTIONAL - Success page header
"el": "Έχουμε λάβει την προσφορά σας",
"en": "We have received your offer",
"tr": "We have received your offer"
},
"successEmailHeader": { //<-- OPTIONAL - Success email header
"el": "Έχουμε λάβει την προσφορά σας",
"en": "We have received your offer",
"tr": "We have received your offer"
},
"description": { //<-- Service description (meta)
"el": "[Υποβάλετε αίτηση για ...]",
"en": "[Submit an application ...]",
"tr": ""
},
"url": "https://gov.cy", //<-- URL in (meta, for example `og:url`)
"cdn": { //<-- CDN URL and integrity
"dist": "https://cdn.jsdelivr.net/gh/gov-cy/govcy-design-system@3.2.0/dist",
"cssIntegrity": "sha384-qjx16YXHG+Vq/NVtwU2aDTc7DoLOyaVNuOHrwA3aTrckpM/ycxZoR5dx7ezNJ/Lv",
"jsIntegrity": "sha384-tqEyCdi3GS4uDXctplAd7ODjiK5fo2Xlqv65e8w/cVvrcBf89tsxXFHXXNiUDyM7"
},
"submissionDataVersion": "1", //<-- Submission data version
"rendererVersion": "1.16.1", //<-- govcy-frontend-renderer version
"designSystemsVersion": "3.2.0", //<-- govcy-design-system version
"homeRedirectPage": { //<-- Home redirect page
"el": "https://www.gov.cy/service/aitisi-gia-taftotita/",
"en": "https://www.gov.cy/en/service/issue-an-id-card/",
"tr": "https://www.gov.cy/en/service/issue-an-id-card/"
},
"copyrightText": { //<-- Copyright text
"el": "Κυπριακή Δημοκρατία, 2025",
"en": "Republic of Cyprus, 2025",
"tr": "Republic of Cyprus, 2025"
},
"submissionAPIEndpoint": { //<-- Submission API endpoint
"url": "TEST_SUBMISSION_API_URL",
"method": "POST",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY",
"response": {
"errorResponse": {
"102": {
"error": "user not administrator",
"page": "/test/user-not-admin"
},
"105": {
"error": "user not registration",
"page": "/test/user-not-registered"
}
}
}
},
"submissionGetAPIEndpoint": { //<-- Submission GET API endpoint for temporary saving
"url": "TEST_SUBMISSION_GET_API_URL",
"method": "GET",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
},
"submissionPutAPIEndpoint": { //<-- Submission PUT API endpoint for temporary saving
"url": "TEST_SUBMISSION_PUT_API_URL",
"method": "PUT",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
},
"fileUploadAPIEndpoint": { //<-- File upload API endpoint
"url": "TEST_UPLOAD_FILE_API_URL",
"method": "POST",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
},
"fileDownloadAPIEndpoint": { //<-- File download API endpoint
"url": "TEST_DOWNLOAD_FILE_API_URL",
"method": "GET",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
},
"fileDeleteAPIEndpoint": { //<-- File delete API endpoint
"url": "TEST_DELETE_FILE_API_URL",
"method": "DELETE",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
},
"eligibilityAPIEndpoints": [ //<-- Eligibility API endpoints
{
"url": "TEST_ELIGIBILITY_2_API_URL",
"method": "GET",
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY",
"cashingTimeoutMinutes": "60",
"params": {},
"response": {
"errorResponse": {
"105": {
"error": "user not registration",
"page": "/test/user-not-registered"
}
}
}
}
]
},
"pages": [ //<-- Pages
{
"pageData": { //<-- 1st Page's data (form)
"url": "index", // Page URL
"title": { // Page title
"el": "Επιλογή Εγγάφου",
"en": "Document selection",
"tr": ""
},
"layout": "layouts/govcyBase.njk", // Page layout
"mainLayout": "two-third", // Page main layout
"nextPage": "data-entry-radios" // The next page's URL
},
"pageTemplate": { //<-- Page template
"sections": [ //<-- Page sections
{
"name": "main", //<-- Main section
"elements": [ //<-- Main section elements
{
"element": "form", // Form element
"params": {
"elements": [ // Elements inside the form
{
"element": "checkboxes", // Checkboxes element
"params": { // Checkboxes parameters
"id": "certificate_select",
"name": "certificate_select",
"legend": {
"el": "Τι έγγραφα επιθυμείτε να εκδώσετε;",
"en": "What documents do you wish to issue?"
},
"items": [
{
"value": "birth",
"text": {
"el": "Πιστοποιητικό γέννησης",
"en": "Birth certificate",
"tr": ""
},
"hint": {
"el": "Αν η γέννηση έγινε στην Κύπρο ή στο εξωτερικό και έχει ενημερωθεί το μητρώο του Αρχείου Πληθυσμού ",
"en": "For a birth in Cyprus or abroad which Civil Registry is updated with "
}
},
{
"value": "permanent_residence",
"text": {
"el": "Βεβαίωση μόνιμης διαμονής",
"en": "Certificate of permanent residence",
"tr": ""
},
"hint": {
"el": "Για όσους είναι εγγεγραμμένοι στον εκλογικό κατάλογο",
"en": "For those registered in the electoral list"
}
},
{
"value": "student_proof_of_origin",
"text": {
"el": "Βεβαίωση καταγωγής",
"en": "Certificate of origin",
"tr": ""
},
"hint": {
"el": "Για αίτηση σε πανεπιστήμια στην Ελλάδα",
"en": "To apply to a university in Greece"
}
}
],
"isPageHeading": true,
"hint": {
"el": "Επιλέξτε ένα ή περισσότερα έγγραφα",
"en": "Select one or more documents",
"tr": ""
}
},
"validations": [ // Checkboxes validations
{
"check": "required",
"params": {
"checkValue": "",
"message": {
"el": "Επιλέξετε ένα ή περισσότερα έγγραφα",
"en": "Select one or more documents",
"tr": ""
}
}
}
]
},
{
"element": "button",
"params": {
"id": "continue",
"variant": "primary",
"text": {
"el": "Συνέχεια",
"en": "Continue"
}
}
}
]
}
}
]
}
]
}
},
{
"pageData": { //<-- 2nd Page's data (form)
"url": "data-entry-radios",
"title": {
"el": "Στοιχεία επικοινωνίας ",
"en": "Contact details",
"tr": ""
},
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third",
"nextPage": "review"
},
"pageTemplate": {
"sections": [
{
"name": "beforeMain",
"elements": [
{
"element": "backLink",
"params": {}
}
]
},
{
"name": "main",
"elements": [
{
"element": "form",
"params": {
"elements": [
{
"element": "radios",
"params": {
"id": "mobile_select",
"name": "mobile_select",
"legend": {
"el": "Σε ποιο κινητό μπορούμε να επικοινωνήσουμε μαζί σας;",
"en": "What mobile number can we use to contact you?"
},
"items": [
{
"value": "mobile",
"text": {
"el": "Στο [99 123456]",
"en": "You can use [99 123456]",
"tr": ""
}
},
{
"value": "other",
"text": {
"el": "Θα δώσω άλλο αριθμό",
"en": "I will give a different number",
"tr": ""
},
"conditionalElements": [
{
"element": "fileInput",
"params": {
"id": "proof",
"name": "proof",
"label": {
"el": "Αποδεικτικό τηλεφώνου",
"en": "Telephone proof",
"tr": ""
},
"isPageHeading": false,
"hint": {
"el": "PDF, JPG, JPEG, PNG, είναι οι αποδεκτές μορφές",
"en": "PDF, JPG, JPEG, PNG are the acceptable formats",
"tr": ""
}
},
"validations": [
{
"check": "required",
"params": {
"checkValue": "",
"message": {
"el": "Ανεβάστε τον αποδεικτικό τηλεφώνου",
"en": "Upload the telephone proof",
"tr": ""
}
}
}
]
}
]
}
],
"isPageHeading": true
},
"validations": [
{
"check": "required",
"params": {
"checkValue": "",
"message": {
"el": "Επιλέξετε αν θέλετε να χρησιμοποιήσετε το τηλέφωνο που φαίνεται εδώ, ή κάποιο άλλο",
"en": "Choose if you'd like to use the phone number shown here, or a different one",
"tr": ""
}
}
}
]
},
{
"element": "button",
"params": {
"id": "continue",
"variant": "primary",
"text": {
"el": "Συνέχεια",
"en": "Continue"
}
}
}
]
}
}
]
}
]
}
},
{
"pageData": { //<-- 3rd Page's data (not a form)
"url": "user-not-registered",
"title": {
"el": "Δεν είστε εγγεγραμμένοι ",
"en": "You are not an registered",
"tr": ""
},
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third"
},
"pageTemplate": {
"sections": [
{
"name": "beforeMain",
"elements": []
},
{
"name": "main",
"elements": [
{
"element": "textElement",
"params": {
"id": "title",
"type": "h1",
"text": {
"el": "Δεν είστε εγγεγραμμένοι",
"en": "You are not registered"
}
}
},
{
"element": "htmlElement",
"params": {
"id": "body",
"text": {
"el": "<p>Για να υποβάλετε σε υπηρεσία αυτή, χρειάζεται να είστε εγγεγραμμένοι στο ΧΥΖ.</p>",
"en": "<p>To submit in this service you need to be registered at XYZ.</p>"
}
}
}
]
}
]
}
},
{
"pageData": { //<-- 4th Page's data (not a form)
"url": "user-not-admin",
"title": {
"el": "Δεν είστε διαχειριστής ",
"en": "You are not an administrator",
"tr": ""
},
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third"
},
"pageTemplate": {
"sections": [
{
"name": "beforeMain",
"elements": []
},
{
"name": "main",
"elements": [
{
"element": "textElement",
"params": {
"id": "title",
"type": "h1",
"text": {
"el": "Δεν είστε διαχειριστής ",
"en": "You are not an administrator"
}
}
},
{
"element": "htmlElement",
"params": {
"id": "body",
"text": {
"el": "<p>Για να υποβάλετε σε υπηρεσία αυτή, χρειάζεται να είστε διαχειριστής στο ΧΥΖ.</p>",
"en": "<p>To submit in this service you need to be an administrator of XYZ.</p>"
}
}
}
]
}
]
}
}
]
}
```
Here are some details explaining the JSON structure:
- `site` object: Contains information about the site, including the site ID, language, and footer links. See [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer/tree/main#site-and-page-meta-data-explained) for more details. Some fields that are only specific to the govcy-express-forms project are the following:
- `usesDSFSubmissionPlatform`: A boolean that indicates whether the service uses the DSF submission platform (transforms submission data as needed)
- `cyLoginPolicies`: which types of authenticated CY Login profiles are allowed to access the service
- `submissionDataVersion` : The submission data version,
- `rendererVersion` : The govcy-frontend-renderer version,
- `designSystemsVersion` : The govcy-design-system version,
- `homeRedirectPage`: An object mapping language codes to URLs. When a user visits the root route (e.g., `https://whatever-your-service-is.service.gov.cy/`), the system redirects to the URL for the user's language. If the user's language is not found, it falls back to `"el"` or the first available URL. If not provided, a list of available sites is shown. Example:
```json
"homeRedirectPage": {
"el": "https://www.gov.cy/service/aitisi-gia-taftotita/",
"en": "https://www.gov.cy/en/service/issue-an-id-card/"
}
```
- `eligibilityAPIEndpoints` : An array of API endpoints, to be used for service eligibility. See more on the [Eligibility API Endoints](#%EF%B8%8F-site-eligibility-checks) section below.
- `submissionAPIEndpoint`: The submission API endpoint, to be used for submitting the form. See more on the [Submission API Endoint](#-site-submissions) section below.
- `submissionGetAPIEndpoint`: The submission get API endpoint, to be used for getting the submission data. See more on the [temporary save feature](#-temporary-save-feature) section below.
- `submissionPutAPIEndpoint`: The submission put API endpoint, to be used for temporary saving the submission data. See more on the [temporary save feature](#-temporary-save-feature) section below.
- `fileUploadAPIEndpoint`: The file upload API endpoint, to be used for uploading files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
- `fileDownloadAPIEndpoint`: The file download API endpoint, to be used for downloading files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
- `fileDeleteAPIEndpoint`: The file delete API endpoint, to be used for deleting files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
- `pages` array: An array of page objects, each representing a page in the site.
- `pageData` object: Contains the metadata to be rendered on the page. See [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer/tree/main#site-and-page-meta-data-explained) for more details
- `nextPage`: The URL of the next page to be rendered after the user clicks the `continue` button.
- `pageTemplate` object: Contains the page template to be rendered on the page. See [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer/tree/main#json-input-template) for more details
- `elements` array: An array of elements to be rendered on the page. See all supported [govcy-frontend-renderer elements](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md) for more details
A typical service flow that includes pages `index`, `question-1`, `question-2` under the `pages` array in the JSON file looks like this:
```mermaid
flowchart LR
govcy-page --> isAuth{Is User Authenticated?}
isAuth -- Yes<br><br> Eligibility Check --> index([:siteId/index])
isAuth -- No --> cyLogin[cyLogin]
cyLogin -- Eligibility Check --> index
index -- Eligibility Check<br> Validations<br> Conditionals --> question-1[:siteId/question-1]
question-1 -- Eligibility Check<br> Validations<br> Conditionals --> question-2[:siteId/question-2]
question-2 -- Eligibility Check<br> Validations<br> Conditionals --> review[📄:siteId/review <br> <br> Automatically generated]
review -- Eligibility Check<br> All Validations<br> All Conditionals --> success([✅:siteId/success <br> <br> Automatically generated])
```
Some pages are generated automatically by the project, such as the `review` and `success` pages.
------------------------------------------
#### Pages
Pages defined in the JSON file under the `pages` array, they rendered based on the [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer) library, and they are served by the `/:siteId/:pageUrl` route. The `pageData.nextPage` field is used to determine the next page to render.
Here's an example of a page defined in the JSON file:
```json
{
"pageData": {
"url": "index",
"title": {
"el": "Your email",
"en": "Το email σας"
},
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third",
"nextPage": "telephone-number",
"conditions": [
{
"expression": "dataLayer['test-service.inputData.somePage.formData.showExtra'] != 'yes'",
"redirect": "review"
}
]
},
"pageTemplate": {
"sections": [
{
"name": "beforeMain",
"elements": [
{
"element": "backLink",
"params": {}
}
]
},
{
"name": "main",
"elements": [
{
"element": "form",
"params": {
"elements": [
{
"element": "textInput",
"params": {
"label": {
"en": "What is your email?",
"el": "Ποιο είναι το email σας?"
},
"id": "email",
"name": "email",
"hint": {
"en": "We’ll only use this email for this application",
"el": "Θα χρησιμοποιήσουμε το email σας μόνο για αυτήν την υπηρεσία"
},
"type": "email",
"isPageHeading": true,
"fixedWidth": "50"
},
"validations": [
{
"check": "required",
"params": {
"message": {
"en": "Enter your email",
"el": "Εισαγάγετε το email σας"
}
}
},
{
"check": "valid",
"params": {
"checkValue": "email",
"message": {
"en": "Your email must be a valid email address",
"el": "To emial πρέπει να είναι έχει μορφή email address"
}
}
}
]
},
{
"element": "button",
"params": {
"id": "continue",
"variant": "primary",
"text": {
"el": "Συνέχεια",
"en": "Continue"
}
}
}
]
}
}
]
}
]
}
}
```
The above `page` JSON generates a page that looks like the following screenshot:

The JSON structure is based on the [govcy-frontend-renderer's JSON template](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/README.md#json-template-example).
Lets break down the JSON config for this page:
- **pageData** are the page's meta data, such as the URL, title, layout, mainLayout, and nextPage.
- `pageData.url` is the URL of the page, in this case it's `:siteId/index`
- `pageData.title` is the title of the page, in this case it's `Your email`. This will be used in the `review`, `success` pages, the PDF, the email, and the submission platform.
- `pageData.layout` is the layout used to render the page. The project only supports the default layout `layouts/govcyBase.njk`
- `pageData.mainLayout` is the layout of the `main` section of the page, in this case it's `two-third`. It can be either `two-third` or `max-width`,
- `pageData.nextPage` is the next page to redirect to when the user clicks the `continue` button and all validations pass, in this case it will redirect to `/:siteId/telephone-number`
- `pageData.conditions` is the array that defines the [conditional logic](#-conditional-logic)
- **pageTemplate** is the page's template, which is a JSON object that contains the sections and elements of the page. Check out the [govcy-frontend-renderer's documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/README.md) for more details.
- `sections` is an array of sections, which is an array of elements. Sections allowed: `beforeMain`, `main`, `afterMain`.
- `elements` is an array of elements for the said section. Seem more details on the [govcy-frontend-renderer's design elements documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md).
------------------------------------------
#### Task list pages

Task lists act as navigation hubs that show the completion state of one or more pages in the journey. They are defined by adding a `taskList` block alongside `pageData`:
```json
{
"pageData": {
"url": "task-list",
"title": {
"el": "Η αίτησή σας",
"en": "Your application"
},
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third",
"nextPage": "options"
},
"taskList": {
"topElements": [
{
"element": "progressList",
"params": { "id": "steps", "current": "2", "total": "3" }
},
{
"element": "textElement",
"params": {
"type": "h1",
"text": {
"el": "Αίτηση για ISBN (Task List)",
"en": "Apply for an ISBN (Task List)"
}
}
}
],
"taskPages": [
"index",
"book-title",
"publishing-details",
"authors",
"book-theme",
"book-series",
"reprint",
"translation",
"book-form",
"physical-description",
"collaborators"
],
"hasBackLink": true,
"showSkippedTasks": true,
"linkToContinue": false
}
}
```
- `taskPages` is an ordered list of page URLs. Each entry becomes a GOV.CY task row with the page title, link, and status.
- `showSkippedTasks` toggles whether conditionally hidden pages appear in the UI with a “Not applicable” tag.
- `linkToContinue` (default `false`) enables the renderer’s “Continue without completing all sections” link when POST validation fails.
- `topElements` and `hasBackLink` reuse the usual renderer elements and layout helpers so the page can include progress lists, headings, or lead paragraphs.
##### How statuses are calculated
Task list statuses are produced by the same validation logic as the Review page (`computePageTaskStatus`). Each page can be in one of four states: `NOT_STARTED`, `IN_PROGRESS`, `COMPLETED`, or `SKIPPED` (when a page is hidden by conditions or a custom page opts out). The helper inspects the stored session data per page type:
| Page type | When it becomes **NOT_STARTED** | When it is **IN_PROGRESS** | When it is **COMPLETED** |
|-----------|---------------------------------|----------------------------|--------------------------|
| **Standard form pages** | No `formData` captured yet | `formData` exists but field validation fails | Field validation succeeds (identical to Review) |
| **Multiple things hubs** | No items and the hub hasn’t been posted yet | Fails per-item or min/max validation | All min/max checks pass or the user explicitly posts an empty-but-allowed hub |
| **Update my details** | Data has not been fetched/posted | API result present but local validations fail | Data fetched and passes the same validation used on Review |
| **Custom pages** | Missing status in custom session store | `setCustomPageTaskStatus` sets `IN_PROGRESS` | Custom implementation marks it `COMPLETED` |
| **Nested task lists** | Any child task is `NOT_STARTED` | Mixed child statuses or explicit `IN_PROGRESS` | Every child task is `COMPLETED` or `SKIPPED` |
Because the logic reuses the Review pipeline, you only have to define task lists in JSON; they immediately reflect file uploads, conditional skips, posted multiple entries, and custom page states without extra code.
POSTing a task-list page re-runs these computations. If every section is complete, the user is redirected to `pageData.nextPage` (or `/review` when the request came from `?route=review`). Otherwise, the middleware stores a per-task error summary in the session so the renderer can show contextual guidance. Optional `linkToContinue` text and the global “Complete all sections before continuing.” message are also inserted automatically.
#### Form vs static pages
- If the `pageTemplate` includes a `form` element in the `main` section and `button` element, the system will treat it as form and will:
- Perform the eligibility checks
- Display the form
- Collect the form data for the following input elements (more details on the [govcy-frontend-renderer's design elements documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md)):
- `textInput`
- `textArea`
- `select`
- `radios`
- `checkboxes`
- `datePicker`
- `dateInput`
- `fileInput`: the file upload feature must be enabled to use this element (see more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below)
- Validate the form data (see more on the [Input validations](#-input-validations) section below)
- Store the form data in the systems data layer
- Redirect the user to the next page (or `review` page if the user came from the review page)
- Else if the `pageTemplate` does not include a `form` element in the `main` section, the system will treat it as static content and will:
- Not perform the eligibility checks
- Display the static content
When designing form pages, refer to the Unified Design System's [question pages pattern](https://gov-cy.github.io/govcy-design-system-docs/patterns/question_pages/).
**Error pages**
Pages that can be used to display messages when eligibility or submission fail are simply static content pages. That is pages that do not include a `form` element.
**Start page**
The [start page](https://gov-cy.github.io/govcy-design-system-docs/patterns/service_structure/#start-page) should be created in the gov.cy portal and should be defined in the `site.homeRedirectPage` property in the site config JSON file. All pages within a service are private by default and can only be accessed by authenticated users, so the start page cannot be created in the JSON file.
**Notes**:
- Check out the [govcy-frontend-renderer's design elements](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md) for more details on the supported elements and their parameters.
- Check out the [input validations section](#-input-validations) for more details on how to add validations to the JSON file.
-------------------------------------------
#### Update my details pages

Update my details pages are pages that can integrate the [Update My Personal Details service](https://update-my-details.service.gov.cy/) to fetch or update a user’s name, email, mobile number, or correspondence address, without breaking the user journey. The implementation of these pages follow the instructuctions described in the [Use ‘Update my personal details’ in Your Service](https://dsf.dmrid.gov.cy/2025/05/20/use-update-my-personal-details-in-your-service/) post.
If the `scope` also includes the `email` element, the system will also use that email address to send the email to the user on submition.
**Update my details - example JSON config**
```json
{
"pageData": {
"url": "index", // Page URL
"layout": "layouts/govcyBase.njk",
"mainLayout": "two-third",
"nextPage": "next-page"
},
"updateMyDetails": {
"APIEndpoint": { // API endpoint for fetching user details from the Update My Details service
"url": "CIVIL_REGISTRY_CONTACT_API_URL", // URL
"method": "GET", // HTTP method
"clientKey": "DSF_API_GTW_CLIENT_ID", // Client key
"serviceId": "DSF_API_GTW_SERVICE_ID", // Service ID
"dsfgtwApiKey": "DSF_API_GTW_SECRET" // DSF GTW API key
},
"updateMyDetailsURL": "UPDATE_MY_DETAILS_URL", // DOMAIN URL for redirecting to the Update My Details service
"topElements": [ // Elements to be displayed on the top of the page
{
"element": "progressList",
"params": {
"id": "steps",
"current": "4",
"total": "4",
"showSteps": true
}
}
],
"scope": [ // Scope of the form. What elenents to collect
"fullName",
"email",
"mobile",
"address"
],
"hasBackLink": true // Whether the hub page has a back link
}
}
```
Lets break down the JSON config for Update my details:
- **updateMyDetails** are the page's definition for the integration with the Update My Details service.
- `updateMyDetails.APIEndpoint` is the API endpoint for fetching user details from the Update My Details service.
- `updateMyDetails.updateMyDetailsURL` is the DOMAIN URL for redirecting to the Update My Details service.
- `updateMyDetails.scope` is the scope of the form. What elenents to collect. It can be one or more of the following:
- `fullName`
- `dob`
- `email`
- `mobile`
- `address`
- `updateMyDetails.hasBackLink` is a boolean that indicates whether the hub page has a back link.
The above config references the following environment variables that need to be set:
```dotenv
### Update my d