Skip to content

Liber Quartus · Tome III

Project and package manifests

Seven files tell grim what a project is made of. You write one of them, and part of a second.

≈12 min grim 1.0.0
Source

Which file do you edit when you want another module, a second theme, a different server? And which files change under your hands when grim runs? Every manifest has one writer. Once you know who that is, you know whether an edit of yours will last. This tome takes the files one by one. What the version numbers and constraints in them mean is a separate subject, covered in Release trains.

File Written by Read by
grim.json You, and the commands that change the project Almost every command
grim.lock grim install, grim update, grim require grim install, grim update, grim release
core.json The grim-core release Every command that needs the train or the core module list
module.json grim make:module, then the release The resolver, grim install, grim manifest:validate
theme.json grim make:theme, then the release grim install, grim manifest:validate
config/modules-available.php grim install, grim update The application
storage/config/modules-enabled.php grim once, then you The application

grim.json is the one you write

grim create-project writes the first version. After that the file is yours, and it is one of the three files the project repository tracks. Three commands edit it for you: grim require adds to requires and themes, grim remove takes a module out of requires, and grim tune --auto writes a performance block. create-project also writes description and version. Both are labels for people, and no command reads them.

{
    "name": "shop",
    "core": "~2.5",
    "php": "^8.5",
    "requires": { "cart": "~2.5", "payments": "~2.5" },
    "themes": { "frontend": "shop-front:~2.5", "admin": "float:~2.5" },
    "docker": { "services": { "horizon": true, "scheduler": true, "reverb": false } },
    "deploy": {
        "health-timeout": 60,
        "post-deploy": ["php artisan migrate --force"],
        "environments": { "production": { "host": "203.0.113.10", "domain": "shop.example.com" } }
    }
}
Key Means
name Required. Names the containers, the local domain shop.test, the image and the stack on the server.
core Required. The constraint on grim-core, which decides the trains grim update may move to.
php PHP version of the generated Dockerfiles. Leading ^, ~, >= are stripped. Default 8.5.
requires Map of module name to constraint. Core modules are never listed here.
themes The slots frontend and admin.
docker, deploy Local and image settings, and where the project is deployed. Both have their own table below.
tls mode: "custom" with domain, cert, key and optionally mail serves the local project on your own certificate and domain instead of shop.test. See LAN dev box with a custom certificate.
deployment Written by grim server:dev:add-project on a dev box: kind, domain, vite_host, mailpit_host, tls. It switches the compose template. Do not write it by hand.
performance The pm.* values of PHP-FPM. Written by grim tune --auto, read by grim tune.
build.vite theme-frontend and theme-admin name the theme grim build compiles when .env names none. Without both, it is the first theme of the slot.

A theme slot is a string or a list

A theme is written as slug:constraint. A slot with one theme stays a plain string. A slot with several is a list, ["shop-front:~2.5", "blank:~2.5"], and the first entry is the active theme. Every theme of a slot is installed. Which one the application shows is decided by THEME_FRONTEND and THEME_ADMIN in .env. grim build warns when .env names a theme the slot does not declare.

docker

Key Means
services horizon, scheduler and reverb, each true or false. A missing key means true, true, false. Nothing else turns a service on.
owner, image owner is the namespace of the image, ghcr.io/<owner>/grim-shop, default grimoiry. image is the whole name without a tag and wins over owner.
timeout Seconds grim up and grim install wait for an image build and for the containers to start. grim install also reads the finer build_timeout, composer_timeout, up_timeout, yarn_timeout and container_wait.

deploy

Key Means
project The name of the stack on the server, when it should differ from name.
host The server. Without it, the host registered by grim server:add-project is used.
path The stack directory on the server. Default /opt/<stack>.
domain The domain the router answers on. There is no default. A project without a domain does not deploy.
health-timeout, pull-timeout Seconds to wait for the new containers, default 60, and seconds allowed for pulling the image.
post-deploy List of commands run in the app container after the switch. Read from the top of deploy only. A failing one is a warning, not a rollback.
environments Map of environment name to its own host, path, domain, health-timeout and pull-timeout. grim deploy looks here first, then at the top of deploy, then in the server registration. The top-level path and domain are production's and are skipped for every other environment.

A deploy.domain at the top names production only. A staging stack without environments.staging.domain falls back to the domain in your own server registration, and without one the deploy stops with has no domain. Once a project has two environments, keep host and domain under environments so that every machine resolves the same targets. The recipe Staging to production, one image makes that move.

grim.lock is written whole, never edited

grim install, grim update and grim require all end by writing the lock again, complete. There is no command that patches one line of it, and you should not either. Commit it with grim.json.

{
    "generated_at": "2026-03-02T09:14:07+00:00",
    "grim_cli_version": "1.4.0",
    "train": "2.5",
    "core_version": "2.5.0",
    "core_commit": "9f2c1e7a4b3d5c6e8f0a1b2c3d4e5f6a7b8c9d0e",
    "modules": {
        "cart": { "version": "2.5.3", "commit": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", "requires": { "payments": "~2.5" } },
        "payments": { "version": "2.5.1", "commit": "7e8f9a0b1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "requires": [] }
    },
    "themes": { "frontend": [{ "name": "shop-front", "version": "2.5.0", "commit": "c0ffee1234567890abcdef1234567890abcdef12" }] },
    "content_hash": "sha256:5d41402abc4b2a76b9719d911017c592..."
}
Key Means
train The train the project is on. An older lock without it gets it derived from core_version.
core_version, core_commit The exact grim-core release and its commit.
modules Every module in the project, the ones pulled in as dependencies included. Each has version, commit and the requires it declared.
themes Per slot a list of name, version, commit, in the order of grim.json.
generated_at, grim_cli_version, content_hash When the lock was written, by which grim, and a hash of everything except the date.

modules is longer than requires in grim.json. You asked for cart. The lock also names payments, because cart requires it. grim why answers who brought a module in.

core.json comes with core

core.json sits in the project root and arrives with the grim-core release, like the rest of the skeleton. The next train overwrites it. It tells grim two things it cannot know otherwise: which train this core is, and which modules are part of core and therefore never belong in requires. Outside a project there is no core.json on disk, and create-project works from a copy bundled in grim.

{
    "name": "core",
    "version": "2.5.0",
    "php": "^8.5",
    "modules": ["admin", "auth", "common", "users"]
}
Key Means
name Required. Always core.
version Required, X.Y.Z. Its first two parts are the train. make:module and make:theme stamp it into the manifest they write.
php Required. One constraint string. A new grim.json inherits it.
modules The core modules: kebab-case, unique, sorted, never empty.

module.json describes one module

grim make:module writes the first module.json, in the module's own directory under src/. From then on you maintain three of its fields, and the release maintains two.

{
    "name": "cart",
    "namespace": "Cart",
    "version": "2.5.3",
    "type": "module",
    "provider": "CartProvider",
    "category": "commerce",
    "requires": { "payments": "~2.5" },
    "php": { "extensions": ["intl"] }
}
Key Means
name Required, kebab-case. The name used in requires and in grim require cart.
namespace Required, PascalCase. The directory under src/ and the PHP namespace Modules\Cart.
version Required, X.Y.Z. Written by the release. grim install warns when it differs from the tag it checked out.
type Required. module or core-module.
provider Required. PascalCase, ends with Provider. This class name goes into the two module lists.
category One of commerce, insurance, finance, crm, content, communication, hr, ops, integrations, dev. Empty counts as uncategorized.
requires Map of module name to a constraint of the form ~X.Y. Generated by the release from what the code imports. Never written by hand.
php.extensions Extension names as php -m prints them. grim install warns when your PHP lacks one.

The resolver follows requires from module to module, and the result is the modules block of the lock. The scaffold also writes description, routes and migrations. grim does not read them. The keys minimum-core and soft-requires are gone, and grim manifest:validate fails a manifest that still carries one. Run it in the module directory before you push. It applies the required fields and shapes of this table and names every error at once. The recipe Create a module shows the file in use.

theme.json is the shortest

grim make:theme writes it into resources/themes/<type>/<name>/.

{
    "name": "shop-front",
    "version": "2.5.0",
    "type": "frontend",
    "requires-modules": ["frontend"]
}
Key Means
name Required, kebab-case. The slug of slug:constraint.
version Required, X.Y.Z. For a theme that exists only on your disk, this is the version the lock records.
type Required. frontend or admin, the slot the theme fits.
requires-modules List of module names in kebab-case. Checked for shape by manifest:validate.

The two module lists

Both are PHP files that return a list of provider classes, one line per module, such as Modules\Cart\CartProvider::class.

config/modules-available.php is what is installed. grim install and grim update write it again every time, from the resolved modules, taking each class name from provider in module.json. An edit does not survive.

storage/config/modules-enabled.php is what the application boots. grim install creates it as a copy of the first list, only when it does not exist, and --no-enable skips even that. After that three commands touch it, one line at a time: grim require and grim make:module add a module, grim remove takes one out. Everything else in the file is your decision, and no update brings a line back that you deleted. What grim owns in your project has the rest of that division, including why only grim.json and grim.lock are committed.