Skip to content

Your Own Service

Your backend starts as a fork of the service repository. You choose the modules it runs, give every environment its own settings, create the first channel and point the CMS and the storefront at the API. Product names such as Volkanos and zeno are explained in Concepts.

The service is a thin Django project. Modules are packages it installs and enables; none of their code lives in the service repository. You keep main/ as the Django project and change which modules run, the settings and the data.

Diagram

The muted item is planned, not shipped. modules.yaml is where you choose what goes into LOCAL_APPS and the URLs, and the enabled apps decide the queues of the Celery worker. The first channel and your data package are the data the service writes.

Fork entirius-service-volkanos on GitHub (the repository is not a template) and clone your fork. Then, in the clone:

Terminal window
uv sync --frozen
cp main/settings_example.py main/settings_local.py
uv run python manage.py migrate
uv run python manage.py runserver

main/settings_local.py is required and gitignored. It must define ENVIRONMENT, SECRET_KEY and DATABASES. Without the file, or without one of those names, the service refuses to boot and names what is missing: an environment nobody configured on purpose does not start.

The example settings read DATABASE_URL and fall back to SQLite when it is unset, so the commands above need no database server. They also enable no module: LOCAL_APPS is empty, so a module route answers Not Found until its module is enabled.

modules.yaml at the repository root is the registry of every module the service depends on. Each entry carries the facts you need to enable it:

KeyMeaning
coreThe module runs in every service; the rest are optional.
requiresModules that must be enabled with it: hard package dependencies.
also_enableApps it imports whenever their package is installed, so on this service they must be enabled with it.
queuesWorker queues its tasks are routed to.
settingsDjango settings it reads.
urlsWhether main/urls.py routes it when its app is enabled.

Every module package stays installed, whether you enable its app or not. That is why also_enable exists: a module that imports a sibling as soon as the sibling’s package is importable needs that sibling’s app enabled too.

Enabling a module takes three places:

  1. Its package in pyproject.toml. The packages of the registry’s modules are already there.
  2. Its app label in LOCAL_APPS in main/settings_local.py, in registry order, together with the apps of its requires and also_enable. Order matters: every module comes after the modules it requires.
  3. A URL guard in main/urls.py when urls: true. The guards of the registry’s modules are already there; each one routes its module only where the app is enabled.

The commented LOCAL_APPS list in main/settings_example.py is the full set in the right order. It is generated from the registry by make settings-example, so copy from it and delete what you do not run.

The platform entry of the registry is the access module. It is never added to LOCAL_APPS: the service enables it itself wherever the package is installed, and with it the admin gate and the token checks. The package is one of the service’s dependencies since Platform 4.0: a fork gets it by keeping the package installed.

What each module does: Module Catalog.

Every environment has its own main/settings_local.py, and none of them is committed. Beyond the required names, the commented blocks of main/settings_example.py show what a working installation sets:

  • CELERY_BROKER_URL, REDIS_URL and CELERY_RESULT_BACKEND for the worker.
  • QMS_TYPE, the stock strategy of the environment.
  • CRYPT_SALT, the Fernet key of the encrypted fields, required when the crypt module is installed. The example file carries the command that generates one.
  • JWT_SECRET, which defaults to SECRET_KEY. Set it per environment.
  • CORS_ALLOWED_ORIGINS: the CMS and the storefront call the API from the browser, so production lists their origins.
  • DRF_NUM_PROXIES, the number of proxies in front of the application. Left unset, it fails check --deploy.

The read model needs one thing the example file does not show: a django-redis cache as the default cache. The harness sets it in its own settings_local.py, which is a complete working example of the file.

Module settings are listed per module in modules.yaml under settings.

One worker consumes celery, the default queue, plus the queues of every enabled module:

Terminal window
celery -A main worker -l info -Q <list>

<list> is comma separated. Build it from the registry each time you enable a module. What a queue missing from -Q costs: Process Supervision.

A migrated database has no channel, and nothing sells without one. Create a superuser, then run the first-channel script with your copy of scripts/first-channel.example.toml:

[channel]
idx = "web"
name = "Web"
default_language = "en"
languages = ["en", "pl"]
default_currency = "EUR"
currencies = ["EUR"]
country = "DE"
Terminal window
uv run python manage.py createsuperuser
make first-channel CONFIG=my.toml

Languages, currencies and the country are codes of the regional tables. make first-channel runs scripts/first_channel.py on that file, and the script prints one line per step:

first-channel: regional created
first-channel: pim created
first-channel: sync ran
first-channel: pricemanager created
…
first-channel: storefront X-API-KEY ent_api_… access token
first-channel: api-key created
first-channel: admin created
first-channel: munin ran
first-channel: done web
LineMeans
regionalThe regional defaults fixture was loaded: the languages, currencies and countries the config refers to.
pim, pricemanager, checkout, matrix, accounts, qmsThe channel was created in that module. Each module keeps its own channel row, and they agree on the idx.
syncThe channel sync commands of the other enabled modules ran.
storefront X-API-KEYThe storefront’s key, shown once.
adminEvery superuser got the customer record the CMS login reads. createsuperuser does not create it.
muninThe module registry the CMS reads its panels from was refreshed.
doneThe idx of the channel.

A step whose module is not enabled prints skipped, and so does admin while no superuser exists: run createsuperuser, then the script again. A second run prints exists where the first printed created and no key line. It is safe to repeat, with one caveat: the pim step writes the name, defaults, languages and currencies of the config file back on every run, so keep the file in step with what you later change in the CMS.

With the access module installed, the printed X-API-KEY is an application token with the checkout.storefront scope, pinned to the channel. The server stores only a hash and cannot show the value again, so store it now.

Why keys are tokens: Access and API Tokens. Rotation, revocation and more tokens: Application Tokens.

In the CMS .env, set VUE_APP_API_URL to the URL of your service and VUE_APP_CHANNEL to the idx of the channel, then build. Every variable: Admin CMS.

In the storefront’s _CONFIG/ directory, API_BASE_URL is the URL of your service and API_CHECKOUT_KEY of the channel is the key the first-channel script printed. The three files and their keys: Storefront.

In the harness both frontends are configured from its .env: PWA_CHANNEL is the channel the storefront serves and the CMS edits, and CHECKOUT_API_KEY, a line you add, is the storefront’s access token. Set them, then build and start:

Terminal window
make pwa
make cms

The harness seed imports the fixture keys of the demo dataset as access tokens. On your own data, CHECKOUT_API_KEY is the value the first-channel script printed.

A channel without products is an empty shop. Catalog, prices and stock go in through a package of CSV files and a fixed run of manage.py commands: Data Import.

In the harness’s .env, SERVICE and SERVICE_BRANCH select the service under test. The image build clones github.com/entirius/<SERVICE> at that branch, so those two lines choose among the repositories and branches of the entirius organisation.

A fork that lives elsewhere runs in dev mode, where the harness mounts a local clone over the image. Leave SERVICE as it is, clone your fork into repos/services/<SERVICE> (the path make clone fills), then:

Terminal window
make build
make dev

On every dev start the entrypoint copies the harness’s docker/settings_local.py into the mounted clone and syncs the environment from your fork’s lock file. Two consequences. The main/settings_local.py of your clone is overwritten, so edit the harness’s file. And the modules that run in the harness come from the active recipe: that file reads LOCAL_APPS from recipes/<RECIPE>/apps.txt, and the worker consumes the queues of recipes/<RECIPE>/queues.txt (RECIPE in .env, shop when unset). Change modules and queues in those two files, then run make dev again. make dev exits clean even when the service failed to start; the service’s container log tells the truth.

The reference setup and the patterns behind it are under DevOps: host layout, process supervision, watchdogs, the checks a deploy must pass. Install and update scripts are not shipped yet; until they are, the host side is yours to write.

Turn your own catalog into a data package: Your Data.