- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
CitrineOS Server (@citrineos/server)
This is the OCPP server application for CitrineOS — the runnable entrypoint that wires together
@citrineos/base and @citrineos/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.
Table of Contents
- Running the Server
- Attaching a Debugger
- Server Ports
- Database Sync vs. Migration
- Runtime Configuration
- Bootstrap Configuration Environment Variables
- Generating OCPP Interfaces
- Validating Custom OCPP DataTransfer Messages
- Allow Unknown Charging Stations & Auto-Commissioning
- Hasura Metadata
- 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:
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.
Without Docker
To start the server directly with pnpm, run from the repository root:
pnpm run start
Or from this directory:
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:
pnpm run build --prefix ../../ && pnpm run migrate && node --inspect=0.0.0.0:9229 ./dist/index.js
to:
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 — Swagger8081: websocket server TCP connection without auth8082: websocket server TCP connection with basic HTTP auth8083: additional websocket server8443/8444: TLS websocket servers9229: 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:
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, orgcp
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 IDBOOTSTRAP_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 IDBOOTSTRAP_CITRINEOS_FILE_ACCESS_GCP_CREDENTIALS- GCP Credentials object (Optional, if not set will use Application Default Credentials such as theGOOGLE_APPLICATION_CREDENTIALSenvironment 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:<version>.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-enginecontainer (you can do this via the Docker Desktopexecview):
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-enginecontainer:
hasura-cli metadata export
OR
hasura metadata export
- Find the exported files in the
graphql-enginecontainer's files in the metadata filepath<name of project i.e. citrine>/metadataand pull that metadata backup onto your local machine - Copy the contents of the copied
metadatafolder into theapps/Server/hasura-metadatafolder 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.

