product:create
Table of contents
- Introduction
- Getting it running
- Choosing the SPL model
- Creating one while another is selected
- Replacing an existing product
- What it writes
- Advanced
- Reference
- Requirements
- See also
Introduction
product:create scaffolds a new product in the workspace. It writes the Docker files, the startup scripts, src/ and a pyproject.toml pinned to an SPL model. The product has no features yet, and nothing is installed or running.
splent product:create my_first_app --spl sample_splent_spl
✅ Product 'my_first_app' created successfully in /workspace/my_first_app
The name must be unique in the workspace and a lowercase Python identifier (letters, digits and underscores, not starting with a digit). It becomes the package under src/.
Getting it running
product:create only writes the scaffold. Four commands take it from there.
splent product:select my_first_app
splent product:configure
splent product:resolve
splent product:derive --dev
product:select makes it the active product. product:configure chooses its features, product:resolve links them and product:derive --dev builds and starts it. To name the product for one command without selecting it, use SPLENT_APP=my_first_app splent <command> (see Another product for one command).
You can also add features one by one with feature:attach or feature:add.
Choosing the SPL model
splent product:create my_first_app
Without --spl, the command lists every model the workspace knows and asks for one. That includes the working copies, the cache, the models other products pin and the sample line sample_splent_spl, whose DOI ships with the CLI.
When the model is not on disk, it is downloaded from UVLHub once. Its pin (mirror, DOI, concept DOI, version and file name) goes to [tool.splent.spl_model], so any clone can fetch it.
For a model this workspace has never seen, give its DOI.
splent product:create my_first_app --spl notes_spl --spl-doi <doi>
A DOI other than the one the workspace knows records only that DOI and the mirror.
Creating one while another is selected
splent product:create my_second_app --no-select
Without --no-select, the command refuses to run while a product is selected, so you never create one by mistake while you work on another. --no-select creates it and leaves your selection as it is.
Replacing an existing product
splent product:create my_first_app --force
The old tree is moved aside and deleted only once the new one is written, so a failure puts it back. --force only replaces a product, never a feature or a SPLENT repository.
What it writes
<workspace>/
└── <product_name>/
├── .forgejo/workflows/ ← splent-validate.yml, splent-upgrade.yml
├── docker/ ← compose files, Dockerfiles, .env.*.example
├── entrypoints/ ← entrypoint.dev.sh, entrypoint.prod.sh
├── scripts/ ← the numbered startup scripts
├── src/
│ └── <product_name>/
│ ├── __init__.py
│ ├── config.py
│ ├── errors.py
│ ├── logging.py
│ ├── static/ ← css/, js/, favicon.svg
│ └── templates/ ← base_template.html, 400.html, 401.html
├── pyproject.toml
├── package.json
├── .gitignore
└── README.md
The ports come from crc32(<product_name>) % 1000 (see Port offset). The web server gets 5000+offset, the database 33060+offset and Locust 8089+offset. The command warns when one is already used by another product of the workspace.
pyproject.toml
[tool.splent]
cli_version = "<cli version>"
features = []
features_dev = []
features_prod = []
spl = "<spl_name>"
[tool.splent.spl_model]
mirror = "uvlhub.io"
doi = "<doi>"
concept_doi = "<concept doi>"
version = "<version>"
file = "<file>.uvl"
[tool.splent.runtime]
splent_framework = "<framework version>"
splent_cli = "<cli version>"
gunicorn = ">=23,<24"
spl names the model, which never lives in the product. It is read from the working copy splent_spl_<name>/, then .splent_cache/spls/, then downloaded by its DOI. [tool.splent.runtime] is what the production image installs besides the features, and product:build keeps it current.
Startup scripts
The scripts run in numbered order. The dev entrypoint runs 00 to 05_0. The prod entrypoint skips 00, 01 and 02_1 and ends with 05_1.
| Script | Purpose |
|---|---|
00_core_requirements_dev.sh |
Installs the workspace’s framework and CLI editable |
00_install_features.sh |
Installs every declared feature (state installed) |
01_compile_assets.sh |
Builds the frontend assets with Webpack |
02_0_db_wait_connection.sh |
Waits for MariaDB |
02_1_db_create_db_test.sh |
Creates the test database |
02_2_db_create_splent_migrations.sh |
Creates the splent_migrations table |
03_initialize_migrations.sh |
Runs splent db:upgrade once, under a lock of the database server (state migrated). See Startup script integration for SPLENT_MIGRATE_ON_START |
04_handle_migrations.sh |
Seeds a fresh database with splent db:seed -y when RUN_DB_SEED=true, outside production |
05_0_start_app_dev.sh |
Starts the Flask dev server |
05_1_start_app_prod.sh |
Starts the production server |
RUN_DB_SEED is true in .env.dev.example and false in .env.prod.example. A development product seeds itself the first time it starts on an empty database, and never after. Run db:seed to seed by hand.
To bring these scripts into an existing product, run product:sync-template.
Docker files
The dev Dockerfile installs the workspace’s framework and CLI editable. The prod Dockerfile builds with the product as its only context, installs the versions of [tool.splent.runtime] and copies the SPL model from docker/spl/ (see the SPL model in the image).
The production compose file runs ${SPLENT_IMAGE:-<product_name>:latest}, the local build unless deploy/.env names a published image. See Environment files for what .env.prod.example carries.
The .gitignore keeps every real env file out of git. It keeps in what product:build writes for a clean clone (deploy/, docker/requirements.lock, splent.contracts.json and docker/spl/), so commit them when a build changes them.
Error pages
templates/400.html and 401.html are the pages the framework renders for those statuses. The 404 and 500 pages come from the theme, through errors.py, so they wear the product’s skin.
There is no 403.html. The framework draws the 403 page inside base_template.html, an application shell, so the user keeps the sidebar. check:product warns about any link to a file of static/ that does not exist.
Continuous integration
.forgejo/workflows/ holds two Forgejo Actions workflows. They run once the product’s repository is on the forge.
| Workflow | Runs | What it does |
|---|---|---|
splent-validate.yml |
On every push to main, every pull request, and by hand |
Resolves and validates the product, checks the committed deploy artifacts match product:build --no-image, audits the bundles and runs check:vendor. Other jobs run splent lint --target app and the tests |
splent-upgrade.yml |
On the upgrade_schedule cron (Mondays at 06:00 UTC by default), and by hand |
Runs feature:upgrade --yes. When pyproject.toml changed, it pushes the branch splent/feature-upgrades and opens a pull request against main |
Neither workflow releases or deploys. Features are cloned anonymously, so they must be readable without a token. You can tune both workflows in pyproject.toml.
[tool.splent.ci]
upgrade_schedule = "0 3 * * *" # every day at 03:00 UTC
tests = false # no tests job
Without tests, the tests job is left out when it cannot run a server the features need (see The servers the features need). After a change, run splent product:sync-template <product> --ci to write it into the workflows (see product:sync-template).
While a workflow stays as written, the commands that change the features keep it in step. An edited one is left alone (see Keeping the workflows in step).
License
Nothing product:create writes carries a license, so add the LICENSE you choose. The framework and the features of the SPLENT catalog are LGPL-3.0-or-later, which lets your product carry any license, a closed one included.
Advanced
When the model cannot be downloaded, the product is still created, with a warning to fetch the model before you derive. When no DOI is known, the product only derives on a machine that has the model on disk, so publish it with
spl:publishand runspl:pin.
Reference
Options
| Option | Description |
|---|---|
--spl NAME |
The SPL model to derive from. Without it, the command asks |
--spl-doi DOI |
The DOI of a model this workspace has never seen |
--force |
Replace an existing product |
--no-select |
Create it while another product is selected, and leave that one selected |
--features-file PATH |
Accepted and ignored |
Errors
| Message | What to do |
|---|---|
A product is currently selected: <product> |
Run splent product:deselect, or pass --no-select |
'<name>' is not a valid product name |
Use lowercase letters, digits and underscores, starting with a letter or underscore |
The product '<name>' already exists at <path> |
Pick another name, or pass --force |
'<name>' is the name of a SPLENT repository, not a product |
Pick another name. --force never replaces splent_framework, splent_cli, splent_docs, a splent_feature_* or a splent_spl_* |
<path> is not a SPLENT product: <reason> |
--force only replaces a directory whose pyproject.toml has [tool.splent] and no [tool.splent.contract]. Nothing was changed |
This workspace knows no SPL models. |
Start one with splent spl:create <name>, or pass --spl <name> --spl-doi <doi> |
Requirements
- Detached mode, or
--no-select. The command never selects the product it creates. Runsplent product:deselectfirst, orSPLENT_APP= splent product:create <product_name>to run it detached once.
See also
- The Tutorials walk through a product from creation to deployment.