Modules and Composition
A Vix application grows by composition.
You start with a small app.
Then you add what the app needs:
core
json
http
validation
middleware
database
websocket
p2p
sync
cache
cryptoThe important rule is simple:
the application should declare what it usesNo hidden guessing.
No unclear magic.
A module should be visible in the project model.
The base idea
A Vix app is not one giant block.
It is composed from smaller capabilities.
The model:
application
-> modules
-> dependencies
-> configuration
-> runtimeA backend can use HTTP.
A real-time app can use WebSocket.
A database app can use SQLite or MySQL.
A distributed app can use P2P.
The application stays one app, but its capabilities are composed.
What a module means
A module is a reusable capability.
Examples:
| Module | Role |
|---|---|
core | base types, runtime primitives, utilities |
json | JSON parsing, serialization, values |
http | HTTP server, routes, request, response |
validation | request and data validation |
middleware | request pipeline behavior |
db | database access and migrations |
log | logging and structured output |
websocket | real-time connections |
p2p | peer-to-peer networking |
sync | offline-first synchronization |
cache | memory, file, HTTP, or build-related cache |
crypto | hashing, signatures, encryption helpers |
A module should have a clear job.
If it does too many unrelated things, composition becomes hard.
Application modules in vix.app
For simple apps, modules should be declared in vix.app.
Example:
name = "api"
type = "executable"
cpp_standard = "23"
sources = [
"src/main.cpp",
"src/app/AppFactory.cpp",
"src/routes/HealthRoutes.cpp"
]
include_dirs = [
"src"
]
modules = [
"core",
"json",
"http",
"validation",
"middleware",
"db",
"log"
]This tells Vix what the application needs.
The build workflow can then wire the right targets.
The runtime workflow can then run an app that was built with the correct capabilities.
Why modules must be explicit
Hidden modules create confusion.
Bad model:
Vix guesses what the app needs.Better model:
The app declares what it needs.
Vix wires it correctly.This matters when debugging.
If the app uses JSON, it should be visible.
If the app uses database support, it should be visible.
If the app needs WebSocket, it should be visible.
Explicit modules make the app easier to understand.
Built-in modules vs registry packages
There are two different ideas:
Vix modules
registry packagesThey are not the same.
| Item | Meaning |
|---|---|
| Vix module | A capability provided by the Vix ecosystem or runtime |
| Registry package | A versioned external package installed through the Vix registry |
Example module declaration:
modules = [
"core",
"json",
"http"
]Example registry dependency:
vix add softadastra/jsonThe mental model:
modules = capabilities used by the app
packages = versioned dependencies installed into the projectDependency files
Registry dependencies are tracked by two files:
vix.json
vix.lockvix.json stores declared dependency requirements.
vix.lock stores exact resolved versions.
The workflow:
vix registry sync
vix add softadastra/json
vix installAfter that, the project has a reproducible dependency state.
After cloning:
vix installThat is the command that matters.
Do not use vix update when you only want to reproduce the project.
vix install vs vix update
This distinction is important.
vix install = install what is already locked
vix update = resolve newer versions and change the lockfileAfter cloning a project:
vix installWhen you intentionally want newer versions:
vix update --installA serious project should not randomly update dependencies during normal setup.
Reproducibility starts with vix.lock.
Adding a dependency
Use vix add when the project needs a new package.
vix add softadastra/jsonWith a version:
vix add softadastra/json@1.0.0With a range:
vix add softadastra/json@^1.0.0What changes:
vix.json
vix.lock
.vix/deps/
.vix/vix_deps.cmakeThen validate:
vix build
vix check --testsRemoving a dependency
Use:
vix remove softadastra/jsonThen reinstall project state:
vix installThen validate:
vix check --testsRemoving a dependency is not only a CLI action.
You must also remove source code that depends on it:
#include statements
types
function calls
CMake references when using CMake directlyChecking outdated dependencies
Use:
vix outdatedRefresh the registry first:
vix registry sync
vix outdatedFor CI:
vix outdated --strictThis checks whether packages are outdated.
It does not update them.
That is the correct behavior.
Searching packages
Use:
vix search jsonSearch uses the local registry index.
So sync first:
vix registry sync
vix search softadastraExample:
softadastra/json (latest: 1.0.0)
JSON support for Softadastra and Vix applications.
repo: https://github.com/softadastra/jsonSearch is offline after the registry index exists locally.
Registry model
The registry model has three parts:
remote registry repository
local registry index
local package storevix registry sync updates local metadata.
vix registry syncvix store manages local package content.
vix store path
vix store gc --project --dry-runThe model:
registry index = package metadata
store = cached package content
project = selected dependenciesPublishing packages
A reusable library can be published to the registry.
Basic flow:
vix fmt --check
vix check --tests
vix build --preset release
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
vix publish 0.2.0 --dry-run
vix publish 0.2.0 --notes "Add helpers"Publishing expects:
clean Git working tree
local tag
tag pushed to origin
registry index synced
package metadata availableIf registry metadata is missing locally:
vix registry syncPackage metadata
A package should have clear metadata.
Example vix.json for a library:
{
"namespace": "softadastra",
"name": "json",
"version": "1.0.0",
"type": "header-only",
"include": "include",
"license": "MIT",
"description": "JSON support for Softadastra and Vix applications.",
"keywords": ["cpp", "json", "vix"],
"deps": []
}Important fields:
| Field | Role |
|---|---|
namespace | package owner or organization |
name | package name |
version | package version |
type | package type |
include | include root |
license | license |
description | package description |
keywords | search keywords |
deps | package dependencies |
Keep metadata boring and accurate.
That is better than marketing text.
Package identity
A package id uses this form:
namespace/nameExample:
softadastra/json
softadastra/core
softadastra/fsScoped form is also accepted by package commands:
@softadastra/jsonBut the identity is still:
softadastra/jsonUse stable names.
A package name should not change every week.
Version model
Package versions should follow SemVer-style versions.
Example:
0.1.0
1.0.0
1.2.3Git tags use the v prefix:
v0.1.0
v1.0.0
v1.2.3Commands usually accept the version without the v in publish:
vix publish 1.0.0The Git tag is:
v1.0.0Keep this distinction clear.
Lockfile model
A lockfile should contain exact resolved versions.
Conceptually:
{
"lockVersion": 1,
"dependencies": [
{
"id": "softadastra/json",
"requested": "^1.0.0",
"version": "1.0.3",
"repo": "https://github.com/softadastra/json",
"tag": "v1.0.3",
"commit": "abc123",
"hash": "..."
}
]
}The exact fields can evolve.
The purpose does not change:
same lockfile
-> same dependency versions
-> reproducible project setupStore model
The local store keeps package checkouts.
Common path:
~/.vix/store/gitInspect it:
vix store pathPreview cleanup:
vix store gc --project --dry-runRun cleanup:
vix store gc --projectBe careful with project-scoped cleanup.
It keeps dependencies referenced by the current project lockfile and may remove other cached entries.
Preview first.
Global packages
Some packages can be installed globally:
vix install -g softadastra/jsonList global packages:
vix list -gUpgrade a global package:
vix upgrade -g softadastra/jsonUninstall a global package:
vix uninstall -g softadastra/jsonGlobal packages are stored outside the current project.
They are not the same as project dependencies.
Project dependencies vs global packages
Keep this distinction:
| Scope | Commands | Storage |
|---|---|---|
| Project | vix add, vix install, vix update, vix remove, vix list | current project and .vix/ |
| Global | vix install -g, vix upgrade -g, vix uninstall -g, vix list -g | ~/.vix/global/ |
Use project dependencies for application builds.
Use global packages only when the package is meant to be available globally.
Composition inside the source tree
Modules and packages should not create a messy source tree.
A backend can stay organized like this:
src/
├── main.cpp
├── app/
├── config/
├── routes/
├── middleware/
├── validation/
├── database/
├── services/
└── errors/Each folder should have one reason to exist.
Do not put all module usage into main.cpp.
Keep main.cpp small.
Composition through an app factory
A clean app can be composed in an app factory.
Header:
#pragma once
#include <vix.hpp>
#include "config/Config.hpp"
namespace api
{
vix::App create_app(const Config &config);
}Implementation:
#include "app/AppFactory.hpp"
#include "routes/HealthRoutes.hpp"
#include "routes/UserRoutes.hpp"
#include "middleware/RequestIdMiddleware.hpp"
namespace api
{
vix::App create_app(const Config &config)
{
vix::App app;
register_request_id_middleware(app);
register_health_routes(app, config);
register_user_routes(app, config);
return app;
}
}The application is composed in one clear place.
Route composition
Routes should be grouped by feature.
routes/
├── HealthRoutes.hpp
├── AuthRoutes.hpp
├── UserRoutes.hpp
└── AdminRoutes.hppEach group exposes one registration function.
Example:
#pragma once
#include <vix.hpp>
#include "config/Config.hpp"
namespace api
{
void register_health_routes(vix::App &app, const Config &config);
}This gives the app factory a simple job:
register_health_routes(app, config);No giant route file.
No giant main.cpp.
Middleware composition
Middleware belongs in its own folder.
middleware/
├── RequestIdMiddleware.hpp
├── CorsMiddleware.hpp
├── AuthMiddleware.hpp
└── RateLimitMiddleware.hppMiddleware should be explicit.
Example:
register_request_id_middleware(app);
register_cors_middleware(app, config);
register_auth_middleware(app, config);The order matters.
So the app factory should show the order.
Validation composition
Validation should not be hidden inside handlers.
A backend can have:
validation/
├── AuthValidation.hpp
├── UserValidation.hpp
└── CommonRules.hppRoute handlers become easier to read.
The model:
parse request
-> validate input
-> call service
-> return responseValidation is a module-level capability, but the app should decide where validation rules live.
Database composition
Database code should not be scattered everywhere.
A backend can use:
database/
├── Database.hpp
├── Database.cpp
├── migrations/
└── repositories/Or:
repositories/
├── UserRepository.hpp
└── SessionRepository.hppMigration commands:
vix db status
vix db migrate
vix db backupORM tooling:
vix orm migrate --db api --dir ./migrations
vix orm status --db api --dir ./migrationsThe app should not know every SQL detail in route handlers.
WebSocket composition
A WebSocket app should keep real-time logic separate.
Example:
websocket/
├── ChatGateway.hpp
├── PresenceGateway.hpp
└── WsEvents.hppVix commands for WebSocket checks:
vix ws check
vix health websocketIf an app uses WebSocket, the app model should show:
configured WebSocket URL
local WebSocket endpoint
public WebSocket endpoint
heartbeat behavior
timeout behaviorThis belongs in configuration, not random code.
P2P composition
P2P apps need node identity, listening port, discovery, and bootstrap behavior.
Example command:
vix p2p --id A --listen 9001With bootstrap:
vix p2p --id A \
--listen 9001 \
--bootstrap on \
--registry http://127.0.0.1:8080 \
--announce onFor source layout:
p2p/
├── NodeConfig.hpp
├── PeerHandlers.hpp
└── BootstrapConfig.hppThe module gives the capability.
The app controls the topology.
Sync composition
Offline-first systems need sync as a separate concern.
The sync model can include:
outbox
WAL
retry policy
conflict handling
network probe
sync engineDo not mix sync logic randomly into handlers.
A clean app can have:
sync/
├── SyncEngineFactory.hpp
├── OutboxHandlers.hpp
└── ConflictPolicy.hppThe important model:
local write first
durable operation
sync later
converge safelyThis is a serious composition layer.
Cache composition
Cache should be explicit.
There are different cache types:
HTTP cache
application data cache
build artifact cache
object cache
registry package storeDo not mix them.
In an app, cache code can live in:
cache/
├── UserCache.hpp
├── ResponseCache.hpp
└── CachePolicy.hppFor Vix local state, inspect cache through:
vix info
vix store path
vix store gc --project --dry-runBuild cache is not the same as application cache.
Crypto composition
Crypto should be isolated and reviewed carefully.
Example layout:
crypto/
├── PasswordHash.hpp
├── TokenSigner.hpp
└── KeyLoader.hppDo not scatter crypto logic across routes.
Do not hardcode secrets.
Use environment variables and config.
Check env:
vix env check
vix env check --productionA crypto module gives primitives.
The app must still use them correctly.
Logging composition
Logging should be available everywhere, but configured in one place.
Example config:
VIX_LOG_LEVEL=info
VIX_LOG_FORMAT=kv
VIX_COLOR=autoProduction logs:
vix logs app
vix logs proxy
vix logs errorsThe app should log enough to debug:
startup
configuration summary
route errors
database errors
external calls
shutdownDo not log secrets.
Environment composition
Modules often need environment variables.
Database:
DATABASE_ENGINE=sqlite
DATABASE_DEFAULT_NAME=./data/app.dbServer:
SERVER_HOST=127.0.0.1
SERVER_PORT=8080Security:
JWT_SECRET=change-me
SESSION_SECRET=change-meProduction required variables:
production.env.requiredCheck them:
vix env check --productionComposition is not only code.
It includes configuration.
Production composition
A production app composes several layers:
Vix app
systemd service
Nginx proxy
health checks
logs
deploy workflowCommands:
vix service init
vix proxy nginx init
vix health
vix logs
vix deployProduction config belongs in vix.json.
The app should not require manual server steps that are impossible to repeat.
Template composition
Templates should create the first composition.
A backend template should include:
vix.app
vix.json
.env.example
production.env.required
src/main.cpp
src/app/
src/config/
src/routes/
src/middleware/
src/validation/
src/database/
src/services/
src/errors/
tests/
migrations/
public/
data/The command:
vix new api --template backendshould create a real backend foundation.
Not an empty toy.
Library composition
A library is different from an app.
For a library:
vix new mathlib --libA library should focus on:
public headers
source files when needed
tests
package metadata
versioning
registry publishingTypical layout:
include/
src/
tests/
vix.json
README.md
LICENSEA library can be packed and published.
vix pack
vix verify
vix publish 0.1.0Application composition vs library composition
The difference:
| Project | Main concern |
|---|---|
| Application | run, configure, serve, deploy |
| Library | expose API, test, package, publish |
An application uses modules and packages to provide behavior.
A library exposes reusable behavior to other projects.
Do not design them the same way.
Composition checklist
For any Vix app, check:
Are modules declared?
Are registry dependencies locked?
Is vix install enough after clone?
Is the source tree separated by feature?
Is main.cpp small?
Are routes grouped?
Is middleware order visible?
Is validation separated?
Is database access isolated?
Is env config documented?
Are health checks present?
Are production commands configured?If not, the app is not yet well composed.
Common workflows
Add a package:
vix registry sync
vix add softadastra/json
vix build
vix check --testsInstall after clone:
vix install
vix buildCheck outdated packages:
vix registry sync
vix outdatedUpdate intentionally:
vix update --install
vix check --testsRemove a dependency:
vix remove softadastra/json
vix install
vix check --testsPublish a library:
vix fmt --check
vix check --tests
vix build --preset release
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
vix publish 0.1.0Common mistakes
Treating modules and packages as the same thing
Wrong model:
modules and packages are identicalCorrect model:
modules are capabilities
packages are versioned dependenciesUpdating dependencies after clone
Wrong:
vix updateCorrect:
vix installHiding everything in main.cpp
Wrong:
main.cpp contains routes, config, database, middleware, and startupCorrect:
main.cpp starts the app
app factory composes the app
features live in focused foldersForgetting registry sync
If a package is not found:
vix registry sync
vix add namespace/nameForgetting validation after dependency changes
After changing dependencies:
vix build
vix check --testsWhat you should remember
Composition is how a Vix app grows.
The model:
app
-> modules
-> packages
-> config
-> runtime
-> productionUse modules = [...] in vix.app for app capabilities.
Use vix add for registry dependencies.
Use vix install to reproduce locked dependency state.
Use vix update only when you intentionally want newer versions.
Keep source code separated by responsibility.
Keep main.cpp small.
The core rule:
declare what the app uses
compose it in clear places
keep the workflow reproducible