![CitrineOS Logo](../../logo_white.png#gh-dark-mode-only) ![CitrineOS Logo](../../logo_black.png#gh-light-mode-only) # CitrineOS Server (`@citrineos/server`) This is the OCPP server application for CitrineOS — the runnable entrypoint that wires together [`@citrineos/base`](../../packages/base) and [`@citrineos/core`](../../packages/core) into a deployable service. It hosts the WebSocket endpoints that charging stations connect to, the OCPP message router, the HTTP/REST Data and Message APIs, and the Sequelize database migrations. It is one workspace member of the `citrineos-core` pnpm monorepo. For repository-wide setup (cloning, `pnpm install`, building, the full-stack Docker Compose files, and the operator UI), see the [root README](../../README.md). ## Table of Contents - [Running the Server](#running-the-server) - [With Docker (backend only)](#with-docker-backend-only) - [Without Docker](#without-docker) - [Attaching a Debugger](#attaching-a-debugger) - [Server Ports](#server-ports) - [Database Sync vs. Migration](#database-sync-vs-migration) - [Runtime Configuration](#runtime-configuration) - [Bootstrap Configuration Environment Variables](#bootstrap-configuration-environment-variables) - [Generating OCPP Interfaces](#generating-ocpp-interfaces) - [Validating Custom OCPP DataTransfer Messages](#validating-custom-ocpp-datatransfer-messages) - [Allow Unknown Charging Stations & Auto-Commissioning](#allow-unknown-charging-stations--auto-commissioning) - [Hasura Metadata](#hasura-metadata) - [Testing with EVerest](#testing-with-everest) ## Running the Server Make sure the workspace has been installed and built first (from the repository root: `pnpm install && pnpm run build`). ### With Docker (backend only) `apps/Server/docker-compose.yml` brings up the server plus its supporting services — RabbitMQ, PostgreSQL, MinIO, and Hasura — but **not** the operator UI. The server image is built from local source and the source tree is mounted as volumes for live reload. Run it from this directory: ```shell cd apps/Server docker compose up -d ``` To run the full stack including the operator UI, use one of the root-level Compose files instead — see the [root README](../../README.md#information-on-docker-setup). ### Without Docker To start the server directly with pnpm, run from the repository root: ```shell pnpm run start ``` Or from this directory: ```shell cd apps/Server pnpm run start ``` This launches the server via `nodemon` (see `nodemon.json`), which builds the workspace, runs database migrations, and then starts the process with the Node.js inspector listening on port 9229. CitrineOS requires configuration to allow your OCPP 1.6 and OCPP 2.0.1 compliant charging stations to connect. To change the configuration used outside of Docker, adjust the configuration file at `apps/Server/src/config/envs/local.ts`. Make sure any changes to the local configuration do not make it into your PR. ## Attaching a Debugger Whether you run the application with Docker or locally with pnpm, you can attach a debugger to port 9229 and set breakpoints in the TypeScript code directly from your IDE. To make the process **wait for the debugger to attach** before executing, modify the `nodemon.json` exec command from: ```shell pnpm run build --prefix ../../ && pnpm run migrate && node --inspect=0.0.0.0:9229 ./dist/index.js ``` to: ```shell pnpm run build --prefix ../../ && pnpm run migrate && node --inspect-brk=0.0.0.0:9229 ./dist/index.js ``` ## Server Ports When running, the server container exposes the following ports (see `docker-compose.yml`): - `8080`: webserver HTTP — [Swagger](http://localhost:8080/docs) - `8081`: websocket server TCP connection without auth - `8082`: websocket server TCP connection with basic HTTP auth - `8083`: additional websocket server - `8443` / `8444`: TLS websocket servers - `9229`: Node.js debugger ## Database Sync vs. Migration By default, CitrineOS uses migrations to manage database schema changes. This is the recommended approach for production environments. The `pnpm run migrate` script (run automatically on start via `nodemon.json`) applies Sequelize migrations. For development purposes, you can also use `sync` to automatically synchronize your database schema with the models. Two sync scripts are available at the repository root: - `pnpm run sync-db`: synchronizes the database schema with the models without altering existing tables. Useful for development when you want to quickly update the schema without losing data. - `pnpm run force-sync-db`: drops all tables and recreates them based on the models. Useful when you want to start with a fresh database. **Disclaimer:** Using `sync` in a production environment is not recommended as it can lead to data loss. Always use migrations for production deployments. ## Runtime Configuration Values from configuration files (`local.ts`, `docker.ts`, `swarm.docker.ts`) may be overridden at runtime via environment variables. Environment variables prefixed with `citrineos_` and hierarchically separated by an underscore will override the corresponding value. For example, the amqp URL: ```json util: { (...) messageBroker: { amqp: { url: 'amqp://guest:guest@localhost:5672' (...) } (...) } (...) } ``` may be overridden by setting the environment variable `CITRINEOS_util_messageBroker_amqp_url` (case-insensitive). ## Bootstrap Configuration Environment Variables All environment variables use the `CITRINEOS_` prefix. Additional prefixes can be added by passing the `--env-prefix` argument to nodemon (see `start:instance1` in `package.json`). Here's the complete list of environment variables used in bootstrapping the application (this is not the full system configuration): ### Basic Bootstrap Configuration - `BOOTSTRAP_CITRINEOS_CONFIG_FILENAME` - Name of the main config file (default: `config.json`) - `BOOTSTRAP_CITRINEOS_CONFIG_DIR` - Directory containing the config file (optional) - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE` - Type of file access: `local`, `s3`, or `gcp` ### Database Configuration Database connection details (moved from system config to bootstrap config for better security and 12-factor compliance): - `BOOTSTRAP_CITRINEOS_DATABASE_HOST` - Database host (default: `localhost`) - `BOOTSTRAP_CITRINEOS_DATABASE_PORT` - Database port (default: `5432`) - `BOOTSTRAP_CITRINEOS_DATABASE_NAME` - Database name (default: `citrine`) - `BOOTSTRAP_CITRINEOS_DATABASE_DIALECT` - Database dialect (default: `postgres`) - `BOOTSTRAP_CITRINEOS_DATABASE_USERNAME` - Database username (optional) - `BOOTSTRAP_CITRINEOS_DATABASE_PASSWORD` - Database password (optional) - `BOOTSTRAP_CITRINEOS_DATABASE_SYNC` - Enable database sync (via sequelize) (true/false, default: `false`) - `BOOTSTRAP_CITRINEOS_DATABASE_ALTER` - Enable database alter (via sequelize) (true/false, default: `false`) - `BOOTSTRAP_CITRINEOS_DATABASE_MAX_RETRIES` - Maximum connection retries (default: `3`) - `BOOTSTRAP_CITRINEOS_DATABASE_RETRY_DELAY` - Retry delay in milliseconds (default: `1000`) ### Local File Access When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=local`: - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_DEFAULT_FILE_PATH` - Default file path (default: `/data`) ### S3 File Access When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=s3`: - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_REGION` - AWS region (optional) - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_ENDPOINT` - S3 endpoint URL (for MinIO or custom S3) - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_DEFAULT_BUCKET_NAME` - S3 bucket name (default: `citrineos-s3-bucket`) - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_FORCE_PATH_STYLE` - Force path style (true/false, default: `true`) - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_ACCESS_KEY_ID` - S3 access key ID - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_SECRET_ACCESS_KEY` - S3 secret access key ### GCP File Access When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=gcp`: - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_GCP_PROJECTID` - Project ID - `BOOTSTRAP_CITRINEOS_FILE_ACCESS_GCP_CREDENTIALS` - GCP Credentials object (Optional, if not set will use Application Default Credentials such as the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or gcloud CLI credentials) ## Generating OCPP Interfaces All CitrineOS interfaces for OCPP 1.6, 2.0.1, and 2.1-defined schemas were procedurally generated using a processing script. Schemas are sourced from official OCPP JSON files. As of release 1.8.0, the schema files used by CitrineOS are not the raw output of this function; we have added field-level validation that the official schemas lack. ## Validating Custom OCPP DataTransfer Messages It is possible to add custom JSON schemas to validate the data fields of DataTransfer messages, which are supported by all OCPP versions. In the `apps/Server/src/index.ts` code, there is a function `ajvInstance()` that creates the AJV instance. Here, you could register DataTransfer schemas: ``` import { MyDataTransferRequestSchema } from './path' ... ajvInstance.compile(MyDataTransferRequestSchema); ``` Note: The schema's `$id` field must follow this format: ``` ${protocol}-${dataTransferRequest.vendorId}${dataTransferRequest.messageId ? `-${dataTransferRequest.messageId}` : ''} ``` 'Protocol' is the OCPP websocket subprotocol, i.e. "ocpp1.6", "ocpp2.0.1", or so on. CitrineOS's validation logic assumes that the data field is a string field with JSON structure, and uses `JSON.parse` before validation. Other approaches to custom DataTransfer message types are not supported. ## Allow Unknown Charging Stations & Auto-Commissioning The System Configuration defines websocket servers with certain properties, one of which is 'Allow Unknown Charging Stations', a boolean that permits charging stations which are not commissioned to connect to CitrineOS. This triggers an auto-commissioning flow which creates the station on its first connection, and creates evses and connectors for that station in response to StatusNotifications. This is not recommended for production; it is exclusively for testing and is enabled by the default configuration only on the websocket server at port 8081 — which also has no security. Since not all information on the charger is necessarily available in the OCPP messages, commissioning may be wrong and will be incomplete. In 1.6 in particular, multi-evse stations will not commission properly because 1.6 does not have a concept of 'evses'. This will lead to improper behavior if a 1.6 station with multiple evses is auto-commissioned: CitrineOS will assume each new transaction is on the same evse and will automatically mark older transactions on that evse as inactive, leading to an inconsistent state with the charging station. ## Hasura Metadata In order for Hasura to track the existing Citrine tables and relationships, this repository comes with Hasura metadata already exported into the `apps/Server/hasura-metadata` folder. Running the Docker container will automatically import this metadata and track all tables and relationships. Unfortunately, Hasura doesn't currently support importing metadata from a JSON (which is the format if you export your metadata from the Hasura UI or API). Refer to this issue for more information: https://github.com/hasura/graphql-engine/issues/8423#issuecomment-1115996153. Therefore, you must use the Hasura CLI to re-export your metadata, should something change with it. As explained in the Hasura docs https://hasura.io/docs/2.0/migrations-metadata-seeds/auto-apply-migrations/#auto-apply-metadata, Hasura provides an image called `hasura/graphql-engine:.cli-migrations-v3` that will process and import the metadata first before starting the server and runs the Hasura CLI internally. This is the image CitrineOS normally uses in order to automatically load accurate metadata. However, if you want to capture the current state of your database, you should use a normal version tag (such as `v2.40.3` instead of `v2.40.3.cli-migrations-v3`). Then proceed to the Hasura console at `localhost:8090`, go to the data tab, use the sidebar to navigate to the database schema at default>public, and track all of the tables, relationships, and functions you need. Then proceed with the below instructions. You can follow these steps to re-export your metadata via the Hasura CLI in the `graphql-engine` container: - (if the hasura cli isn't installed): ``` curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash ``` - (If not yet initialized) Initialize the Hasura project in the `graphql-engine` container (you can do this via the Docker Desktop `exec` view): ``` hasura-cli init OR hasura init enter any name you wish for the project (i.e. citrine) ``` - Export the metadata by executing this command in the `graphql-engine` container: ``` hasura-cli metadata export OR hasura metadata export ``` - Find the exported files in the `graphql-engine` container's files in the metadata filepath `/metadata` and pull that metadata backup onto your local machine - Copy the contents of the copied `metadata` folder into the `apps/Server/hasura-metadata` folder in this repository ## Testing with EVerest In case you don't have a charger that supports OCPP to experiment with, you can run the EVerest charger simulator locally and point it at CitrineOS. Helper scripts (`pnpm run start-everest` and `pnpm run start-everest-16`) and full instructions live in [`everest/README.md`](./everest/README.md).