How grim is assembled
A running site is six kinds of thing put together, and only one of them, the smallest, is written for that site alone.
You run one command in an almost empty directory and an application appears: a Laravel skeleton, dozens of modules, two themes, a Docker stack. Where did each part come from, who wrote it, and which of them would you change if something had to change? The parts are few. Most confusion about grim comes from taking an artefact for a source, and editing the copy instead of the thing it was copied from.
Six pieces, six homes
Every name below comes from one rule. Everything lives in the grimoiry organization on GitHub, and every repository starts with grim-.
| Piece | What it is | Where it lives | Describes itself in |
|---|---|---|---|
| grim-core | The application skeleton and the core modules | grimoiry/grim-core |
core.json |
| A module | One feature: a provider, routes, migrations, views | grimoiry/grim-<name>, such as grim-cart |
module.json |
| A theme | The look of one of two slots, frontend or admin | grimoiry/grim-theme-<slug>, grimoiry/grim-theme-admin-<slug> |
theme.json |
| A project | One client, one application | A repository of its own, and the image ghcr.io/grimoiry/grim-<project> |
grim.json, grim.lock |
| The CLI | The tool that assembles the rest | grimoiry/grim-cli, shipped as one PHAR |
Its own version |
| The registry | The portal at grim.grimoiry.com |
A web application, not a package | Nothing on your disk |
The first three are packages. A project is a list of packages. The last two are neither: they are what reads the list and what remembers things no list can hold. The fields of the five files are the subject of Project and package manifests.
Packages are released, not written
Nobody works in grimoiry/grim-cart. Core, modules and themes are developed and released together from one source, and the package repositories are what a release leaves behind: a tree and a tag. That is why they share a version line, which Release trains explains, and why a change to a module never starts in its package repository.
| Piece | Who changes it | How the change travels |
|---|---|---|
| grim-core | The platform maintainers | A new train |
| A shared module or theme | Anyone, from inside a project | grim push turns your edit into a pull request against the source, and a release brings it back as a patch |
| A module or theme of your own | You | grim make:module and grim make:theme scaffold it in the project and can give it a repository |
| A project | The team of that client | A commit to grim.json and grim.lock |
| The CLI | The platform maintainers | A release of the PHAR, fetched by grim self-update |
core.json is the clearest case of an artefact. It carries the version of core, the PHP it needs and the list of core modules, and grim reads it to tell a core module from one you may require or remove. Nobody edits it. It is generated at release and lands in the project with the rest of core.
A project is the only piece written for one site
A project is two files. grim.json names the core train, the modules, the themes for each slot, the PHP version and the environments. grim.lock records what those names resolved to. Everything else in the directory is fetched or rendered from them, which is the argument of What grim owns in your project.
Two more things belong to a project and are not in its repository. The first is its image, built by grim build and named ghcr.io/grimoiry/grim-<project> unless grim.json names another owner or a whole image. The second is what each environment keeps for itself: a .env, a database, uploaded files.
| Artefact | Made from | Made by |
|---|---|---|
| A package tag | The shared source | A release |
grim.lock |
grim.json and the tags that exist |
grim install, grim require, grim update |
| The project directory | grim.lock and the templates in the CLI |
grim install |
| The image | The project directory | grim build |
| A running environment | The image, and that server's .env and data |
grim deploy |
Read the table downwards and each row is the input of the next. Nothing in it flows back up, so a fix belongs in the highest row where the fault exists.
The CLI carries the infrastructure, not the application
The CLI holds no application code. It holds the resolver that turns constraints into a lock, the installers, and the templates of everything around the application: the Dockerfiles, the compose file, the nginx configuration, the stack a server runs, the scaffold of a new module or theme.
This is why it has a version line of its own. A new train changes what the application is. A new CLI changes how it is built and served, and grim upgrade is the moment a project takes that in. What the CLI keeps about you is outside any project, in ~/.grim: your GitHub token, your settings, the servers you have registered, a cached catalogue.
Where the pieces meet on disk
Assembly puts each package in a fixed place. The place is derived from the name, and nothing in it is configurable.
| Path | Holds | Put there by |
|---|---|---|
app/, bootstrap/, config/, routes/, core.json |
The skeleton | A copy of the core release |
src/<Name>/ |
One module per directory, core and required alike. cart becomes src/Cart/, namespace Modules\Cart |
Core modules with the copy, the rest as a git checkout at a tag |
resources/themes/frontend/<slug>/, resources/themes/admin/<slug>/ |
Every theme of each slot | A git checkout at a tag |
config/modules-available.php |
The provider class of every required module, read from its module.json |
Generated on install and update |
storage/config/modules-enabled.php |
The modules that are switched on | Created once as a copy of the list above, then only added to |
docker/, docker-compose.yml |
The stack | Rendered from the templates in the CLI |
Core modules and required modules sit side by side under src/ and look the same. The difference is in who lists them. Core modules are named by core itself and cannot be switched off. Required modules are named by your grim.json.
Where they meet at runtime
grim is absent at runtime. Once the files are in place the application is an ordinary Laravel application, and it puts itself together from three lists.
| Question | Answered by |
|---|---|
| Which core modules load? | The list that ships inside core. Always all of them. |
| Which other modules load? | storage/config/modules-enabled.php, or config/modules-available.php when that file does not exist |
| Which theme renders? | THEME_FRONTEND and THEME_ADMIN in .env. A slot in grim.json may install several themes. The first one is written into a fresh .env, and after that .env decides. |
Each provider registers the routes, migrations and views of its module. That is the whole mechanism. A module that is installed but not enabled is files on disk and nothing more.
Installing a module and enabling it are separate acts. grim require enables the modules you named and not the ones they pulled in. A dependency is on disk, locked and listed as available, and stays off until its line is in the enabled list. Add, trace and remove modules shows the line.
The registry holds what a repository cannot
The catalogue is not kept in the registry. grim search and grim list:available read the repositories of the organization on GitHub, with the token from grim auth:login, and cache the answer in ~/.grim for a day. What you can see there is what your account has been given access to. The same token is what the registry accepts, so there is no second login.
| The CLI talks to the registry about | When |
|---|---|
| Shared secrets, credentials every project needs and no repository should hold | grim secrets:set stores one. grim install writes them into the local .env, and says nothing when it cannot. |
| A heartbeat: project, environment, modules, active themes, core version, PHP, host and URL | After each successful grim release. A failed heartbeat never fails a release. |
| Error reports from the CLI | When a command crashes, unless you have turned telemetry off |
| Access to a private package | Never by itself. A refused install prints the address where you ask. |
A project can point at another registry with registry_url in grim.json. Without the registry, install, update, build and deploy all still work. You lose the shared secrets and the overview of what runs where.
Questions
Is grim-core a module?
No. It is versioned like one and installed differently. A module is a checkout you can edit and send back. Core is copied over the project, and the core modules inside it arrive with the copy.
Can a project hold code that is in no package?
Yes. A module scaffolded under src/ is part of the project until you give it a repository. Track it in the project repository as What grim owns in your project describes, and the recipe Create a module covers the rest.
Where does a new project come from?
From the CLI alone. grim create-project writes a grim.json for the train of the day, and the first install does everything in the tables above. The recipe Create a project walks through it.
Related tomes
What grim owns in your project
Which files in a project are yours, which grim rewrites, which command rewrites them, and why the repository of a project holds almost nothing.
Release trains
Core, every module and every theme share one version line. What a train is, what moves it, and what a constraint in grim.json really allows.
Project and package manifests
grim.json, grim.lock, core.json, module.json, theme.json and the two generated module lists, each with its writer, its readers, a minimal example and its fields.
Spells that do it
grim install
Assemble the whole project
Turn grim.json into a working project by fetching core, every module and theme, writing grim.lock, building the image and starting the stack.
grim require
Summon a module or theme
Add a module or theme to grim.json, install it with everything it depends on, and rewrite grim.lock, all inside the train the project is on.
grim build
Forge the production image
Build the project's production Docker image, tag it with the next version, and push it to GHCR when asked.